auth repair
This commit is contained in:
parent
5eb73ddb37
commit
3dcc2f903e
@ -6,66 +6,89 @@ from src.db.session import get_db
|
||||
from src.domain.schemas import LoginRequest, RefreshRequest, Token
|
||||
from src.services.auth_service import AuthService
|
||||
|
||||
# if not settings.DEBUG:
|
||||
# from raisa_fastapi_protected_api import UserInfo, get_user_dependency
|
||||
if not settings.DEBUG:
|
||||
from raisa_fastapi_protected_api import UserInfo, get_user_dependency
|
||||
|
||||
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(
|
||||
login_data: LoginRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
async def _login_via_keycloak_email(db: AsyncSession, user: "UserInfo") -> Token:
|
||||
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(login_data.username, login_data.password)
|
||||
token = await auth_service.authenticate_user_via_email(user.email)
|
||||
if not token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Неверное имя пользователя или пароль",
|
||||
detail=_PROD_USER_NOT_FOUND,
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
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),
|
||||
# ):
|
||||
# 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
|
||||
if settings.DEBUG:
|
||||
|
||||
|
||||
@router.post("/login-form", response_model=Token)
|
||||
async def login_form(request: Request, db: AsyncSession = Depends(get_db)):
|
||||
form = await request.form()
|
||||
username = form.get("username")
|
||||
password = form.get("password")
|
||||
if not username or not password:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="username/password required",
|
||||
@router.post("/login", response_model=Token)
|
||||
async def login(
|
||||
login_data: LoginRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
auth_service = AuthService(db)
|
||||
token = await auth_service.authenticate_user(
|
||||
login_data.username, login_data.password
|
||||
)
|
||||
auth_service = AuthService(db)
|
||||
token = await auth_service.authenticate_user(username, password)
|
||||
if not token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Неверное имя пользователя или пароль",
|
||||
)
|
||||
return token
|
||||
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()
|
||||
username = form.get("username")
|
||||
password = form.get("password")
|
||||
if not username or not password:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
detail="username/password required",
|
||||
)
|
||||
auth_service = AuthService(db)
|
||||
token = await auth_service.authenticate_user(username, password)
|
||||
if not token:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Неверное имя пользователя или пароль",
|
||||
)
|
||||
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)
|
||||
|
||||
@ -12,8 +12,6 @@ from src.repository.user_repository import UserRepository
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
# if settings.DEBUG:
|
||||
|
||||
async def get_user_by_token(
|
||||
token: str,
|
||||
db: AsyncSession,
|
||||
@ -46,28 +44,37 @@ async def get_user_by_token(
|
||||
)
|
||||
return user
|
||||
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> AppUser:
|
||||
return await get_user_by_token(token=credentials.credentials, db=db)
|
||||
|
||||
# else:
|
||||
# from raisa_fastapi_protected_api import UserInfo, get_user_dependency
|
||||
if settings.DEBUG:
|
||||
|
||||
# async def get_current_user(
|
||||
# user: UserInfo = Depends(get_user_dependency),
|
||||
# db: AsyncSession = Depends(get_db),
|
||||
# ) -> AppUser:
|
||||
# user_repo = UserRepository(db)
|
||||
# db_user = await user_repo.get_by_email(user.email)
|
||||
# if db_user is None:
|
||||
# raise HTTPException(
|
||||
# status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
# detail="Пользователь не найден",
|
||||
# headers={"WWW-Authenticate": "Bearer"},
|
||||
# )
|
||||
# return db_user
|
||||
async def get_current_user(
|
||||
credentials: HTTPAuthorizationCredentials = Depends(security),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> AppUser:
|
||||
return await get_user_by_token(token=credentials.credentials, db=db)
|
||||
|
||||
else:
|
||||
from raisa_fastapi_protected_api import UserInfo, get_user_dependency
|
||||
|
||||
async def get_current_user(
|
||||
user: UserInfo = Depends(get_user_dependency),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> AppUser:
|
||||
if not getattr(user, "email", None):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Не удалось идентифицировать пользователя: в токене отсутствует поле email",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
user_repo = UserRepository(db)
|
||||
db_user = await user_repo.get_by_email(user.email)
|
||||
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:
|
||||
|
||||
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)
|
||||
|
||||
# if not settings.DEBUG:
|
||||
# from raisa_fastapi_protected_api import (
|
||||
# AuthorizationMiddleware,
|
||||
# OpenEndpoint,
|
||||
# ProtectedOAuthSettings,
|
||||
# ProtectedRolesSettings,
|
||||
# ProtectedSettings,
|
||||
# SearchType,
|
||||
# )
|
||||
if not settings.DEBUG:
|
||||
from raisa_fastapi_protected_api import (
|
||||
AuthorizationMiddleware,
|
||||
OpenEndpoint,
|
||||
ProtectedOAuthSettings,
|
||||
ProtectedRolesSettings,
|
||||
ProtectedSettings,
|
||||
SearchType,
|
||||
)
|
||||
|
||||
|
||||
_AUDIT_CLEANUP_LOCK_KEY = 21987431
|
||||
@ -104,13 +104,13 @@ async def lifespan(app: FastAPI):
|
||||
cleanup_task: asyncio.Task | None = None
|
||||
if not settings.DEBUG:
|
||||
await create_tables()
|
||||
# ProtectedSettings(
|
||||
# ProtectedOAuthSettings(JWKS_URI=settings.JWKS_URL),
|
||||
# ProtectedRolesSettings(
|
||||
# APP_NAMESPACE=settings.APP_NAMESPACE,
|
||||
# APP_NAME=settings.APP_NAME,
|
||||
# ),
|
||||
# )
|
||||
ProtectedSettings(
|
||||
ProtectedOAuthSettings(JWKS_URI=settings.JWKS_URL),
|
||||
ProtectedRolesSettings(
|
||||
APP_NAMESPACE=settings.APP_NAMESPACE,
|
||||
APP_NAME=settings.APP_NAME,
|
||||
),
|
||||
)
|
||||
cleanup_task = asyncio.create_task(_audit_cleanup_loop())
|
||||
try:
|
||||
yield
|
||||
@ -185,41 +185,61 @@ async def readyz(response: Response):
|
||||
|
||||
|
||||
|
||||
# if not settings.DEBUG:
|
||||
# app.add_middleware(
|
||||
# AuthorizationMiddleware,
|
||||
# open_endpoints=[
|
||||
# OpenEndpoint(path="", type_search=SearchType.ABSOLUTE),
|
||||
# OpenEndpoint(path=settings.ROOT_PATH + "", type_search=SearchType.ABSOLUTE),
|
||||
# OpenEndpoint(path="/", type_search=SearchType.ABSOLUTE),
|
||||
# OpenEndpoint(path=settings.ROOT_PATH + "/", type_search=SearchType.ABSOLUTE),
|
||||
# OpenEndpoint(path="/healthcheck", type_search=SearchType.ABSOLUTE),
|
||||
# OpenEndpoint(
|
||||
# path=settings.ROOT_PATH + "/healthcheck",
|
||||
# type_search=SearchType.ABSOLUTE,
|
||||
# ),
|
||||
# OpenEndpoint(path="/healthz", type_search=SearchType.ABSOLUTE),
|
||||
# OpenEndpoint(
|
||||
# path=settings.ROOT_PATH + "/healthz",
|
||||
# type_search=SearchType.ABSOLUTE,
|
||||
# ),
|
||||
# OpenEndpoint(path="/readyz", type_search=SearchType.ABSOLUTE),
|
||||
# OpenEndpoint(
|
||||
# path=settings.ROOT_PATH + "/readyz",
|
||||
# type_search=SearchType.ABSOLUTE,
|
||||
# ),
|
||||
# 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,
|
||||
# ),
|
||||
# ],
|
||||
# )
|
||||
if not settings.DEBUG:
|
||||
app.add_middleware(
|
||||
AuthorizationMiddleware,
|
||||
open_endpoints=[
|
||||
OpenEndpoint(path="", type_search=SearchType.ABSOLUTE),
|
||||
OpenEndpoint(path=settings.ROOT_PATH + "", type_search=SearchType.ABSOLUTE),
|
||||
OpenEndpoint(path="/", type_search=SearchType.ABSOLUTE),
|
||||
OpenEndpoint(path=settings.ROOT_PATH + "/", type_search=SearchType.ABSOLUTE),
|
||||
OpenEndpoint(path="/healthcheck", type_search=SearchType.ABSOLUTE),
|
||||
OpenEndpoint(
|
||||
path=settings.ROOT_PATH + "/healthcheck",
|
||||
type_search=SearchType.ABSOLUTE,
|
||||
),
|
||||
OpenEndpoint(path="/healthcheck2", type_search=SearchType.ABSOLUTE),
|
||||
OpenEndpoint(
|
||||
path=settings.ROOT_PATH + "/healthcheck2",
|
||||
type_search=SearchType.ABSOLUTE,
|
||||
),
|
||||
OpenEndpoint(path="/healthz", type_search=SearchType.ABSOLUTE),
|
||||
OpenEndpoint(
|
||||
path=settings.ROOT_PATH + "/healthz",
|
||||
type_search=SearchType.ABSOLUTE,
|
||||
),
|
||||
OpenEndpoint(path="/readyz", type_search=SearchType.ABSOLUTE),
|
||||
OpenEndpoint(
|
||||
path=settings.ROOT_PATH + "/readyz",
|
||||
type_search=SearchType.ABSOLUTE,
|
||||
),
|
||||
OpenEndpoint(path="/static", type_search=SearchType.START),
|
||||
OpenEndpoint(
|
||||
path=settings.ROOT_PATH + "/static",
|
||||
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(
|
||||
CORSMiddleware,
|
||||
|
||||
@ -26,10 +26,10 @@ class AuthService:
|
||||
|
||||
async def authenticate_user_via_email(self, email: str) -> Optional[Token]:
|
||||
user = await self.user_repo.authenticate_via_email(email)
|
||||
if not user:
|
||||
if not user or not user.is_active:
|
||||
return None
|
||||
access_token = create_access_token(data={"sub": user.email})
|
||||
refresh_token = create_refresh_token(data={"sub": user.email})
|
||||
access_token = create_access_token(data={"sub": user.username})
|
||||
refresh_token = create_refresh_token(data={"sub": user.username})
|
||||
return Token(
|
||||
access_token=access_token,
|
||||
refresh_token=refresh_token,
|
||||
@ -41,16 +41,19 @@ class AuthService:
|
||||
if payload is None:
|
||||
return None
|
||||
|
||||
username: str | None = payload.get("sub")
|
||||
if username is None:
|
||||
subject: str | None = payload.get("sub")
|
||||
if subject is None:
|
||||
return None
|
||||
|
||||
user = await self.user_repo.get_by_username(username)
|
||||
if not user:
|
||||
user = await self.user_repo.get_by_username(subject)
|
||||
if user is None:
|
||||
user = await self.user_repo.get_by_email(subject)
|
||||
if user is None or not user.is_active:
|
||||
return None
|
||||
|
||||
access_token = create_access_token(data={"sub": user.username})
|
||||
new_refresh_token = create_refresh_token(data={"sub": user.username})
|
||||
token_sub = user.email if subject == user.email else user.username
|
||||
access_token = create_access_token(data={"sub": token_sub})
|
||||
new_refresh_token = create_refresh_token(data={"sub": token_sub})
|
||||
return Token(
|
||||
access_token=access_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
|
||||
|
||||
from src.core.security import create_refresh_token, verify_token
|
||||
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")
|
||||
|
||||
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