auth repair
This commit is contained in:
parent
5eb73ddb37
commit
3dcc2f903e
@ -6,21 +6,46 @@ from src.db.session import get_db
|
|||||||
from src.domain.schemas import LoginRequest, RefreshRequest, Token
|
from src.domain.schemas import LoginRequest, RefreshRequest, Token
|
||||||
from src.services.auth_service import AuthService
|
from src.services.auth_service import AuthService
|
||||||
|
|
||||||
# if not settings.DEBUG:
|
if not settings.DEBUG:
|
||||||
# from raisa_fastapi_protected_api import UserInfo, get_user_dependency
|
from raisa_fastapi_protected_api import UserInfo, get_user_dependency
|
||||||
|
|
||||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||||
|
|
||||||
|
_PROD_USER_NOT_FOUND = "Пользователь не найден в локальной базе данных"
|
||||||
|
_PROD_EMAIL_MISSING = (
|
||||||
|
"Не удалось идентифицировать пользователя: в токене отсутствует поле email"
|
||||||
|
)
|
||||||
|
|
||||||
# if settings.DEBUG:
|
|
||||||
|
|
||||||
@router.post("/login", response_model=Token)
|
async def _login_via_keycloak_email(db: AsyncSession, user: "UserInfo") -> Token:
|
||||||
async def login(
|
if not getattr(user, "email", None):
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=_PROD_EMAIL_MISSING,
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
auth_service = AuthService(db)
|
||||||
|
token = await auth_service.authenticate_user_via_email(user.email)
|
||||||
|
if not token:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail=_PROD_USER_NOT_FOUND,
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
|
||||||
|
@router.post("/login", response_model=Token)
|
||||||
|
async def login(
|
||||||
login_data: LoginRequest,
|
login_data: LoginRequest,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
auth_service = AuthService(db)
|
auth_service = AuthService(db)
|
||||||
token = await auth_service.authenticate_user(login_data.username, login_data.password)
|
token = await auth_service.authenticate_user(
|
||||||
|
login_data.username, login_data.password
|
||||||
|
)
|
||||||
if not token:
|
if not token:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
@ -29,27 +54,8 @@ async def login(
|
|||||||
)
|
)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
# else:
|
@router.post("/login-form", response_model=Token)
|
||||||
|
async def login_form(request: Request, db: AsyncSession = Depends(get_db)):
|
||||||
# @router.post("/login", response_model=Token)
|
|
||||||
# async def login(
|
|
||||||
# login_data: LoginRequest,
|
|
||||||
# db: AsyncSession = Depends(get_db),
|
|
||||||
# user: UserInfo = Depends(get_user_dependency),
|
|
||||||
# ):
|
|
||||||
# auth_service = AuthService(db)
|
|
||||||
# token = await auth_service.authenticate_user_via_email(user.email)
|
|
||||||
# if not token:
|
|
||||||
# raise HTTPException(
|
|
||||||
# status_code=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
# detail="Неверное имя пользователя или пароль",
|
|
||||||
# headers={"WWW-Authenticate": "Bearer"},
|
|
||||||
# )
|
|
||||||
# return token
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login-form", response_model=Token)
|
|
||||||
async def login_form(request: Request, db: AsyncSession = Depends(get_db)):
|
|
||||||
form = await request.form()
|
form = await request.form()
|
||||||
username = form.get("username")
|
username = form.get("username")
|
||||||
password = form.get("password")
|
password = form.get("password")
|
||||||
@ -67,6 +73,23 @@ async def login_form(request: Request, db: AsyncSession = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
@router.post("/login", response_model=Token)
|
||||||
|
async def login(
|
||||||
|
login_data: LoginRequest,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user: UserInfo = Depends(get_user_dependency),
|
||||||
|
):
|
||||||
|
return await _login_via_keycloak_email(db, user)
|
||||||
|
|
||||||
|
@router.post("/login-form", response_model=Token)
|
||||||
|
async def login_form(
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
user: UserInfo = Depends(get_user_dependency),
|
||||||
|
):
|
||||||
|
return await _login_via_keycloak_email(db, user)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/refresh", response_model=Token)
|
@router.post("/refresh", response_model=Token)
|
||||||
async def refresh_token(
|
async def refresh_token(
|
||||||
|
|||||||
@ -12,8 +12,6 @@ from src.repository.user_repository import UserRepository
|
|||||||
security = HTTPBearer()
|
security = HTTPBearer()
|
||||||
|
|
||||||
|
|
||||||
# if settings.DEBUG:
|
|
||||||
|
|
||||||
async def get_user_by_token(
|
async def get_user_by_token(
|
||||||
token: str,
|
token: str,
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
@ -46,28 +44,37 @@ async def get_user_by_token(
|
|||||||
)
|
)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
async def get_current_user(
|
|
||||||
|
if settings.DEBUG:
|
||||||
|
|
||||||
|
async def get_current_user(
|
||||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
) -> AppUser:
|
) -> AppUser:
|
||||||
return await get_user_by_token(token=credentials.credentials, db=db)
|
return await get_user_by_token(token=credentials.credentials, db=db)
|
||||||
|
|
||||||
# else:
|
else:
|
||||||
# from raisa_fastapi_protected_api import UserInfo, get_user_dependency
|
from raisa_fastapi_protected_api import UserInfo, get_user_dependency
|
||||||
|
|
||||||
# async def get_current_user(
|
async def get_current_user(
|
||||||
# user: UserInfo = Depends(get_user_dependency),
|
user: UserInfo = Depends(get_user_dependency),
|
||||||
# db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
# ) -> AppUser:
|
) -> AppUser:
|
||||||
# user_repo = UserRepository(db)
|
if not getattr(user, "email", None):
|
||||||
# db_user = await user_repo.get_by_email(user.email)
|
raise HTTPException(
|
||||||
# if db_user is None:
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
# raise HTTPException(
|
detail="Не удалось идентифицировать пользователя: в токене отсутствует поле email",
|
||||||
# status_code=status.HTTP_401_UNAUTHORIZED,
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
# detail="Пользователь не найден",
|
)
|
||||||
# headers={"WWW-Authenticate": "Bearer"},
|
user_repo = UserRepository(db)
|
||||||
# )
|
db_user = await user_repo.get_by_email(user.email)
|
||||||
# return db_user
|
if db_user is None or not db_user.is_active:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||||
|
detail="Пользователь не найден",
|
||||||
|
headers={"WWW-Authenticate": "Bearer"},
|
||||||
|
)
|
||||||
|
return db_user
|
||||||
|
|
||||||
|
|
||||||
async def get_current_active_user(current_user: AppUser = Depends(get_current_user)) -> AppUser:
|
async def get_current_active_user(current_user: AppUser = Depends(get_current_user)) -> AppUser:
|
||||||
|
|||||||
122
api/src/main.py
122
api/src/main.py
@ -25,15 +25,15 @@ from src.db.session import create_tables
|
|||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
# if not settings.DEBUG:
|
if not settings.DEBUG:
|
||||||
# from raisa_fastapi_protected_api import (
|
from raisa_fastapi_protected_api import (
|
||||||
# AuthorizationMiddleware,
|
AuthorizationMiddleware,
|
||||||
# OpenEndpoint,
|
OpenEndpoint,
|
||||||
# ProtectedOAuthSettings,
|
ProtectedOAuthSettings,
|
||||||
# ProtectedRolesSettings,
|
ProtectedRolesSettings,
|
||||||
# ProtectedSettings,
|
ProtectedSettings,
|
||||||
# SearchType,
|
SearchType,
|
||||||
# )
|
)
|
||||||
|
|
||||||
|
|
||||||
_AUDIT_CLEANUP_LOCK_KEY = 21987431
|
_AUDIT_CLEANUP_LOCK_KEY = 21987431
|
||||||
@ -104,13 +104,13 @@ async def lifespan(app: FastAPI):
|
|||||||
cleanup_task: asyncio.Task | None = None
|
cleanup_task: asyncio.Task | None = None
|
||||||
if not settings.DEBUG:
|
if not settings.DEBUG:
|
||||||
await create_tables()
|
await create_tables()
|
||||||
# ProtectedSettings(
|
ProtectedSettings(
|
||||||
# ProtectedOAuthSettings(JWKS_URI=settings.JWKS_URL),
|
ProtectedOAuthSettings(JWKS_URI=settings.JWKS_URL),
|
||||||
# ProtectedRolesSettings(
|
ProtectedRolesSettings(
|
||||||
# APP_NAMESPACE=settings.APP_NAMESPACE,
|
APP_NAMESPACE=settings.APP_NAMESPACE,
|
||||||
# APP_NAME=settings.APP_NAME,
|
APP_NAME=settings.APP_NAME,
|
||||||
# ),
|
),
|
||||||
# )
|
)
|
||||||
cleanup_task = asyncio.create_task(_audit_cleanup_loop())
|
cleanup_task = asyncio.create_task(_audit_cleanup_loop())
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
@ -185,41 +185,61 @@ async def readyz(response: Response):
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
# if not settings.DEBUG:
|
if not settings.DEBUG:
|
||||||
# app.add_middleware(
|
app.add_middleware(
|
||||||
# AuthorizationMiddleware,
|
AuthorizationMiddleware,
|
||||||
# open_endpoints=[
|
open_endpoints=[
|
||||||
# OpenEndpoint(path="", type_search=SearchType.ABSOLUTE),
|
OpenEndpoint(path="", type_search=SearchType.ABSOLUTE),
|
||||||
# OpenEndpoint(path=settings.ROOT_PATH + "", type_search=SearchType.ABSOLUTE),
|
OpenEndpoint(path=settings.ROOT_PATH + "", type_search=SearchType.ABSOLUTE),
|
||||||
# OpenEndpoint(path="/", type_search=SearchType.ABSOLUTE),
|
OpenEndpoint(path="/", type_search=SearchType.ABSOLUTE),
|
||||||
# OpenEndpoint(path=settings.ROOT_PATH + "/", type_search=SearchType.ABSOLUTE),
|
OpenEndpoint(path=settings.ROOT_PATH + "/", type_search=SearchType.ABSOLUTE),
|
||||||
# OpenEndpoint(path="/healthcheck", type_search=SearchType.ABSOLUTE),
|
OpenEndpoint(path="/healthcheck", type_search=SearchType.ABSOLUTE),
|
||||||
# OpenEndpoint(
|
OpenEndpoint(
|
||||||
# path=settings.ROOT_PATH + "/healthcheck",
|
path=settings.ROOT_PATH + "/healthcheck",
|
||||||
# type_search=SearchType.ABSOLUTE,
|
type_search=SearchType.ABSOLUTE,
|
||||||
# ),
|
),
|
||||||
# OpenEndpoint(path="/healthz", type_search=SearchType.ABSOLUTE),
|
OpenEndpoint(path="/healthcheck2", type_search=SearchType.ABSOLUTE),
|
||||||
# OpenEndpoint(
|
OpenEndpoint(
|
||||||
# path=settings.ROOT_PATH + "/healthz",
|
path=settings.ROOT_PATH + "/healthcheck2",
|
||||||
# type_search=SearchType.ABSOLUTE,
|
type_search=SearchType.ABSOLUTE,
|
||||||
# ),
|
),
|
||||||
# OpenEndpoint(path="/readyz", type_search=SearchType.ABSOLUTE),
|
OpenEndpoint(path="/healthz", type_search=SearchType.ABSOLUTE),
|
||||||
# OpenEndpoint(
|
OpenEndpoint(
|
||||||
# path=settings.ROOT_PATH + "/readyz",
|
path=settings.ROOT_PATH + "/healthz",
|
||||||
# type_search=SearchType.ABSOLUTE,
|
type_search=SearchType.ABSOLUTE,
|
||||||
# ),
|
),
|
||||||
# OpenEndpoint(path="/openapi.json", type_search=SearchType.START),
|
OpenEndpoint(path="/readyz", type_search=SearchType.ABSOLUTE),
|
||||||
# OpenEndpoint(
|
OpenEndpoint(
|
||||||
# path=settings.ROOT_PATH + "/openapi.json",
|
path=settings.ROOT_PATH + "/readyz",
|
||||||
# type_search=SearchType.START,
|
type_search=SearchType.ABSOLUTE,
|
||||||
# ),
|
),
|
||||||
# OpenEndpoint(path="/docs", type_search=SearchType.START),
|
OpenEndpoint(path="/static", type_search=SearchType.START),
|
||||||
# OpenEndpoint(
|
OpenEndpoint(
|
||||||
# path=settings.ROOT_PATH + "/docs",
|
path=settings.ROOT_PATH + "/static",
|
||||||
# type_search=SearchType.START,
|
type_search=SearchType.START,
|
||||||
# ),
|
),
|
||||||
# ],
|
OpenEndpoint(path="/back-static", type_search=SearchType.START),
|
||||||
# )
|
OpenEndpoint(
|
||||||
|
path=settings.ROOT_PATH + "/back-static",
|
||||||
|
type_search=SearchType.START,
|
||||||
|
),
|
||||||
|
OpenEndpoint(path="/openapi.json", type_search=SearchType.START),
|
||||||
|
OpenEndpoint(
|
||||||
|
path=settings.ROOT_PATH + "/openapi.json",
|
||||||
|
type_search=SearchType.START,
|
||||||
|
),
|
||||||
|
OpenEndpoint(path="/docs", type_search=SearchType.START),
|
||||||
|
OpenEndpoint(
|
||||||
|
path=settings.ROOT_PATH + "/docs",
|
||||||
|
type_search=SearchType.START,
|
||||||
|
),
|
||||||
|
OpenEndpoint(path="/docs-local", type_search=SearchType.START),
|
||||||
|
OpenEndpoint(
|
||||||
|
path=settings.ROOT_PATH + "/docs-local",
|
||||||
|
type_search=SearchType.START,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
app.add_middleware(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
|
|||||||
@ -26,10 +26,10 @@ class AuthService:
|
|||||||
|
|
||||||
async def authenticate_user_via_email(self, email: str) -> Optional[Token]:
|
async def authenticate_user_via_email(self, email: str) -> Optional[Token]:
|
||||||
user = await self.user_repo.authenticate_via_email(email)
|
user = await self.user_repo.authenticate_via_email(email)
|
||||||
if not user:
|
if not user or not user.is_active:
|
||||||
return None
|
return None
|
||||||
access_token = create_access_token(data={"sub": user.email})
|
access_token = create_access_token(data={"sub": user.username})
|
||||||
refresh_token = create_refresh_token(data={"sub": user.email})
|
refresh_token = create_refresh_token(data={"sub": user.username})
|
||||||
return Token(
|
return Token(
|
||||||
access_token=access_token,
|
access_token=access_token,
|
||||||
refresh_token=refresh_token,
|
refresh_token=refresh_token,
|
||||||
@ -41,16 +41,19 @@ class AuthService:
|
|||||||
if payload is None:
|
if payload is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
username: str | None = payload.get("sub")
|
subject: str | None = payload.get("sub")
|
||||||
if username is None:
|
if subject is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
user = await self.user_repo.get_by_username(username)
|
user = await self.user_repo.get_by_username(subject)
|
||||||
if not user:
|
if user is None:
|
||||||
|
user = await self.user_repo.get_by_email(subject)
|
||||||
|
if user is None or not user.is_active:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
access_token = create_access_token(data={"sub": user.username})
|
token_sub = user.email if subject == user.email else user.username
|
||||||
new_refresh_token = create_refresh_token(data={"sub": user.username})
|
access_token = create_access_token(data={"sub": token_sub})
|
||||||
|
new_refresh_token = create_refresh_token(data={"sub": token_sub})
|
||||||
return Token(
|
return Token(
|
||||||
access_token=access_token,
|
access_token=access_token,
|
||||||
refresh_token=new_refresh_token,
|
refresh_token=new_refresh_token,
|
||||||
|
|||||||
232
api/tests/unit/test_auth_api_prod_branch.py
Normal file
232
api/tests/unit/test_auth_api_prod_branch.py
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
|
from fastapi import Depends, FastAPI
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.core.config import settings
|
||||||
|
from src.core.exception_handlers import register_exception_handlers
|
||||||
|
from src.db.session import get_db
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_raisa_stub(email: str = "prod-user@example.com"):
|
||||||
|
module = types.ModuleType("raisa_fastapi_protected_api")
|
||||||
|
|
||||||
|
class UserInfo:
|
||||||
|
def __init__(self, email: str):
|
||||||
|
self.email = email
|
||||||
|
|
||||||
|
async def get_user_dependency():
|
||||||
|
return UserInfo(email)
|
||||||
|
|
||||||
|
module.UserInfo = UserInfo
|
||||||
|
module.get_user_dependency = get_user_dependency
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
def _reload_with_prod_auth(monkeypatch, module_name: str, email: str = "prod-user@example.com"):
|
||||||
|
original_debug = settings.DEBUG
|
||||||
|
previous = sys.modules.get("raisa_fastapi_protected_api")
|
||||||
|
monkeypatch.setattr(settings, "DEBUG", False)
|
||||||
|
monkeypatch.setitem(sys.modules, "raisa_fastapi_protected_api", _prepare_raisa_stub(email))
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
importlib.reload(module)
|
||||||
|
return original_debug, previous, module
|
||||||
|
|
||||||
|
|
||||||
|
def _restore_module(monkeypatch, module_name: str, original_debug, previous):
|
||||||
|
monkeypatch.setattr(settings, "DEBUG", original_debug)
|
||||||
|
if previous is None:
|
||||||
|
sys.modules.pop("raisa_fastapi_protected_api", None)
|
||||||
|
else:
|
||||||
|
sys.modules["raisa_fastapi_protected_api"] = previous
|
||||||
|
importlib.reload(importlib.import_module(module_name))
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_login_non_debug_branch_success_and_unauthorized(monkeypatch):
|
||||||
|
original_debug, previous, auth_module = _reload_with_prod_auth(
|
||||||
|
monkeypatch, "src.api.v1.auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mock_async_session = AsyncMock(spec=AsyncSession)
|
||||||
|
mock_auth_service = MagicMock()
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
register_exception_handlers(app)
|
||||||
|
app.include_router(auth_module.router)
|
||||||
|
|
||||||
|
async def mock_get_db():
|
||||||
|
return mock_async_session
|
||||||
|
|
||||||
|
app.dependency_overrides = {get_db: mock_get_db}
|
||||||
|
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(
|
||||||
|
"src.api.v1.auth.AuthService",
|
||||||
|
MagicMock(return_value=mock_auth_service),
|
||||||
|
)
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
mock_auth_service.authenticate_user_via_email = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"access_token": "a",
|
||||||
|
"refresh_token": "r",
|
||||||
|
"token_type": "bearer",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
response = client.post(
|
||||||
|
"/auth/login",
|
||||||
|
json={"username": "ignored", "password": "ignored"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["access_token"] == "a"
|
||||||
|
mock_auth_service.authenticate_user_via_email.assert_awaited_once_with(
|
||||||
|
"prod-user@example.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_auth_service.authenticate_user_via_email = AsyncMock(return_value=None)
|
||||||
|
response = client.post(
|
||||||
|
"/auth/login",
|
||||||
|
json={"username": "ignored", "password": "ignored"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json()["message"] == (
|
||||||
|
"Пользователь не найден в локальной базе данных"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
_restore_module(monkeypatch, "src.api.v1.auth", original_debug, previous)
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_login_form_non_debug_uses_keycloak_email(monkeypatch):
|
||||||
|
original_debug, previous, auth_module = _reload_with_prod_auth(
|
||||||
|
monkeypatch, "src.api.v1.auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
mock_auth_service = MagicMock()
|
||||||
|
mock_auth_service.authenticate_user = AsyncMock(return_value=None)
|
||||||
|
mock_auth_service.authenticate_user_via_email = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"access_token": "a",
|
||||||
|
"refresh_token": "r",
|
||||||
|
"token_type": "bearer",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
register_exception_handlers(app)
|
||||||
|
app.include_router(auth_module.router)
|
||||||
|
|
||||||
|
async def mock_get_db():
|
||||||
|
return AsyncMock(spec=AsyncSession)
|
||||||
|
|
||||||
|
app.dependency_overrides = {get_db: mock_get_db}
|
||||||
|
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(
|
||||||
|
"src.api.v1.auth.AuthService",
|
||||||
|
MagicMock(return_value=mock_auth_service),
|
||||||
|
)
|
||||||
|
client = TestClient(app)
|
||||||
|
response = client.post("/auth/login-form")
|
||||||
|
assert response.status_code == 200
|
||||||
|
mock_auth_service.authenticate_user_via_email.assert_awaited_once_with(
|
||||||
|
"prod-user@example.com"
|
||||||
|
)
|
||||||
|
mock_auth_service.authenticate_user.assert_not_awaited()
|
||||||
|
finally:
|
||||||
|
_restore_module(monkeypatch, "src.api.v1.auth", original_debug, previous)
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_login_non_debug_rejects_missing_email(monkeypatch):
|
||||||
|
original_debug, previous, auth_module = _reload_with_prod_auth(
|
||||||
|
monkeypatch, "src.api.v1.auth", email=""
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
app = FastAPI()
|
||||||
|
register_exception_handlers(app)
|
||||||
|
app.include_router(auth_module.router)
|
||||||
|
|
||||||
|
async def mock_get_db():
|
||||||
|
return AsyncMock(spec=AsyncSession)
|
||||||
|
|
||||||
|
app.dependency_overrides = {get_db: mock_get_db}
|
||||||
|
client = TestClient(app)
|
||||||
|
response = client.post(
|
||||||
|
"/auth/login",
|
||||||
|
json={"username": "ignored", "password": "ignored"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert "email" in response.json()["message"]
|
||||||
|
finally:
|
||||||
|
_restore_module(monkeypatch, "src.api.v1.auth", original_debug, previous)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_current_user_non_debug_maps_email(monkeypatch):
|
||||||
|
original_debug, previous, deps_module = _reload_with_prod_auth(
|
||||||
|
monkeypatch, "src.api.v1.deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db_user = MagicMock(email="prod-user@example.com", is_active=True)
|
||||||
|
mock_repo = MagicMock()
|
||||||
|
mock_repo.get_by_email = AsyncMock(return_value=db_user)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
register_exception_handlers(app)
|
||||||
|
|
||||||
|
@app.get("/me")
|
||||||
|
async def me(current_user=Depends(deps_module.get_current_user)):
|
||||||
|
return {"email": current_user.email}
|
||||||
|
|
||||||
|
async def mock_get_db():
|
||||||
|
return AsyncMock(spec=AsyncSession)
|
||||||
|
|
||||||
|
app.dependency_overrides = {get_db: mock_get_db}
|
||||||
|
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(deps_module, "UserRepository", MagicMock(return_value=mock_repo))
|
||||||
|
client = TestClient(app)
|
||||||
|
response = client.get("/me")
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()["email"] == "prod-user@example.com"
|
||||||
|
mock_repo.get_by_email.assert_awaited_once_with("prod-user@example.com")
|
||||||
|
finally:
|
||||||
|
_restore_module(monkeypatch, "src.api.v1.deps", original_debug, previous)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_current_user_non_debug_rejects_inactive(monkeypatch):
|
||||||
|
original_debug, previous, deps_module = _reload_with_prod_auth(
|
||||||
|
monkeypatch, "src.api.v1.deps"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
db_user = MagicMock(email="prod-user@example.com", is_active=False)
|
||||||
|
mock_repo = MagicMock()
|
||||||
|
mock_repo.get_by_email = AsyncMock(return_value=db_user)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
register_exception_handlers(app)
|
||||||
|
|
||||||
|
@app.get("/me")
|
||||||
|
async def me(current_user=Depends(deps_module.get_current_user)):
|
||||||
|
return {"email": current_user.email}
|
||||||
|
|
||||||
|
async def mock_get_db():
|
||||||
|
return AsyncMock(spec=AsyncSession)
|
||||||
|
|
||||||
|
app.dependency_overrides = {get_db: mock_get_db}
|
||||||
|
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(deps_module, "UserRepository", MagicMock(return_value=mock_repo))
|
||||||
|
client = TestClient(app)
|
||||||
|
response = client.get("/me")
|
||||||
|
assert response.status_code == 401
|
||||||
|
assert response.json()["message"] == "Пользователь не найден"
|
||||||
|
finally:
|
||||||
|
_restore_module(monkeypatch, "src.api.v1.deps", original_debug, previous)
|
||||||
@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, MagicMock
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from src.core.security import create_refresh_token, verify_token
|
||||||
from src.services.auth_service import AuthService
|
from src.services.auth_service import AuthService
|
||||||
|
|
||||||
|
|
||||||
@ -30,3 +31,77 @@ async def test_refresh_token_returns_none_for_invalid():
|
|||||||
token = await service.refresh_token("bad-token")
|
token = await service.refresh_token("bad-token")
|
||||||
|
|
||||||
assert token is None
|
assert token is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_refresh_token_resolves_user_by_email():
|
||||||
|
db = AsyncMock()
|
||||||
|
service = AuthService(db)
|
||||||
|
service.user_repo = MagicMock()
|
||||||
|
service.user_repo.get_by_username = AsyncMock(return_value=None)
|
||||||
|
user = MagicMock(username="admin", email="admin@example.com", is_active=True)
|
||||||
|
service.user_repo.get_by_email = AsyncMock(return_value=user)
|
||||||
|
|
||||||
|
token = await service.refresh_token(
|
||||||
|
create_refresh_token(data={"sub": "admin@example.com"})
|
||||||
|
)
|
||||||
|
|
||||||
|
assert token is not None
|
||||||
|
assert token.token_type == "bearer"
|
||||||
|
payload = verify_token(token.refresh_token)
|
||||||
|
assert payload is not None
|
||||||
|
assert payload["sub"] == "admin@example.com"
|
||||||
|
service.user_repo.get_by_email.assert_awaited_once_with("admin@example.com")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_refresh_token_resolves_user_by_username():
|
||||||
|
db = AsyncMock()
|
||||||
|
service = AuthService(db)
|
||||||
|
service.user_repo = MagicMock()
|
||||||
|
user = MagicMock(username="admin", email="admin@example.com", is_active=True)
|
||||||
|
service.user_repo.get_by_username = AsyncMock(return_value=user)
|
||||||
|
service.user_repo.get_by_email = AsyncMock(return_value=None)
|
||||||
|
|
||||||
|
token = await service.refresh_token(create_refresh_token(data={"sub": "admin"}))
|
||||||
|
|
||||||
|
assert token is not None
|
||||||
|
payload = verify_token(token.refresh_token)
|
||||||
|
assert payload is not None
|
||||||
|
assert payload["sub"] == "admin"
|
||||||
|
service.user_repo.get_by_email.assert_not_awaited()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_authenticate_user_via_email_uses_username_subject():
|
||||||
|
db = AsyncMock()
|
||||||
|
service = AuthService(db)
|
||||||
|
service.user_repo = MagicMock()
|
||||||
|
service.user_repo.authenticate_via_email = AsyncMock(
|
||||||
|
return_value=MagicMock(
|
||||||
|
username="admin", email="admin@example.com", is_active=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
token = await service.authenticate_user_via_email("admin@example.com")
|
||||||
|
|
||||||
|
assert token is not None
|
||||||
|
payload = verify_token(token.access_token)
|
||||||
|
assert payload is not None
|
||||||
|
assert payload["sub"] == "admin"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_authenticate_user_via_email_rejects_inactive():
|
||||||
|
db = AsyncMock()
|
||||||
|
service = AuthService(db)
|
||||||
|
service.user_repo = MagicMock()
|
||||||
|
service.user_repo.authenticate_via_email = AsyncMock(
|
||||||
|
return_value=MagicMock(
|
||||||
|
username="admin", email="admin@example.com", is_active=False
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
token = await service.authenticate_user_via_email("admin@example.com")
|
||||||
|
|
||||||
|
assert token is None
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user