From 21fbf8581810da2404bc4959ea93a5887ff0b82b Mon Sep 17 00:00:00 2001 From: Dyakov Roman Date: Thu, 16 Mar 2023 13:34:21 +0300 Subject: [PATCH 1/5] Reformat --- .github/workflows/build_and_publish.yml | 4 ++-- Makefile | 13 ++++++++++++- logging_dev.conf | 21 +++++++++++++++++++++ logging_prod.conf | 1 + requirements.txt | 3 +++ services_backend/__main__.py | 3 ++- services_backend/models/__init__.py | 1 + services_backend/models/base.py | 1 + services_backend/models/database.py | 6 ++++-- services_backend/routes/base.py | 1 - services_backend/routes/button.py | 7 ++++--- services_backend/routes/category.py | 7 ++++--- services_backend/settings.py | 2 +- 13 files changed, 56 insertions(+), 14 deletions(-) create mode 100644 logging_dev.conf diff --git a/.github/workflows/build_and_publish.yml b/.github/workflows/build_and_publish.yml index 19ac87e..b67b315 100644 --- a/.github/workflows/build_and_publish.yml +++ b/.github/workflows/build_and_publish.yml @@ -87,7 +87,7 @@ jobs: --network=web \ --env DB_DSN='${{ secrets.DB_DSN }}' \ --env ROOT_PATH='/services' \ - --env GUNICORN_CMD_ARGS='--log-config logging_test.conf' \ + --env GUNICORN_CMD_ARGS='--log-config logging_test.conf' \ --name ${{ env.CONTAITER_NAME }} \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test @@ -129,6 +129,6 @@ jobs: --network=web \ --env DB_DSN='${{ secrets.DB_DSN }}' \ --env ROOT_PATH='/services' \ - --env GUNICORN_CMD_ARGS='--log-config logging_prod.conf' \ + --env GUNICORN_CMD_ARGS='--log-config logging_prod.conf' \ --name ${{ env.CONTAITER_NAME }} \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest diff --git a/Makefile b/Makefile index d16211d..61dde27 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,16 @@ run: - source ./venv/bin/activate && uvicorn --reload --log-level debug services_backend.routes.base:app + source ./venv/bin/activate && uvicorn --reload --log-config logging_dev.conf services_backend.routes.base:app + +configure: venv + source ./venv/bin/activate && pip install -r requirements.dev.txt -r requirements.txt + +venv: + python3.11 -m venv venv + +format: + autoflake -r --in-place --remove-all-unused-imports ./services_backend + isort ./services_backend + black ./services_backend db: docker run -d -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust --name db-services-backend postgres:15 diff --git a/logging_dev.conf b/logging_dev.conf new file mode 100644 index 0000000..7837272 --- /dev/null +++ b/logging_dev.conf @@ -0,0 +1,21 @@ +[loggers] +keys=root + +[handlers] +keys=all + +[formatters] +keys=main + +[logger_root] +level=DEBUG +handlers=all + +[handler_all] +class=StreamHandler +formatter=main +level=DEBUG +args=(sys.stdout,) + +[formatter_main] +format=%(asctime)s %(levelname)-8s %(name)-15s %(message)s diff --git a/logging_prod.conf b/logging_prod.conf index 971f309..37d976a 100644 --- a/logging_prod.conf +++ b/logging_prod.conf @@ -10,6 +10,7 @@ keys=json [logger_root] level=INFO handlers=all +formatter=json [logger_gunicorn.error] level=INFO diff --git a/requirements.txt b/requirements.txt index 74edae6..0be3865 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,6 @@ alembic SQLAlchemy gunicorn logging-profcomff +autoflake +isort +black diff --git a/services_backend/__main__.py b/services_backend/__main__.py index 4db9614..2640c6a 100644 --- a/services_backend/__main__.py +++ b/services_backend/__main__.py @@ -1,6 +1,7 @@ -from .routes.base import app import uvicorn +from .routes.base import app + if __name__ == '__main__': uvicorn.run(app) diff --git a/services_backend/models/__init__.py b/services_backend/models/__init__.py index 72261d9..f387ff3 100644 --- a/services_backend/models/__init__.py +++ b/services_backend/models/__init__.py @@ -1,3 +1,4 @@ from .database import Button, Category + __all__ = ["Button", "Category"] diff --git a/services_backend/models/base.py b/services_backend/models/base.py index 9ce7811..16065b5 100644 --- a/services_backend/models/base.py +++ b/services_backend/models/base.py @@ -1,4 +1,5 @@ import re + from sqlalchemy.ext.declarative import as_declarative, declared_attr diff --git a/services_backend/models/database.py b/services_backend/models/database.py index 7804df1..ce4790a 100644 --- a/services_backend/models/database.py +++ b/services_backend/models/database.py @@ -1,6 +1,8 @@ from __future__ import annotations -from sqlalchemy import Integer, String, ForeignKey -from sqlalchemy.orm import relationship, Mapped, mapped_column + +from sqlalchemy import ForeignKey, Integer, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + from .base import Base diff --git a/services_backend/routes/base.py b/services_backend/routes/base.py index 29ca20e..05582ca 100644 --- a/services_backend/routes/base.py +++ b/services_backend/routes/base.py @@ -14,7 +14,6 @@ title='API управления списком сервисов', description='Программный интерфейс управления списком сервисов в приложении Твой ФФ!', version=__version__, - # Настраиваем интернет документацию root_path=settings.ROOT_PATH if __version__ != 'dev' else '/', docs_url=None if __version__ != 'dev' else '/docs', diff --git a/services_backend/routes/button.py b/services_backend/routes/button.py index 0ae46fe..c73c606 100644 --- a/services_backend/routes/button.py +++ b/services_backend/routes/button.py @@ -1,9 +1,10 @@ -from fastapi import HTTPException, APIRouter +from fastapi import APIRouter, HTTPException from fastapi_sqlalchemy import db -from .models.button import ButtonCreate, ButtonUpdate, ButtonGet -from .models.category import CategoryGet from ..models.database import Button, Category +from .models.button import ButtonCreate, ButtonGet, ButtonUpdate +from .models.category import CategoryGet + button = APIRouter() diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index 524ea2f..bf7a32b 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -1,8 +1,9 @@ -from fastapi import HTTPException, APIRouter +from fastapi import APIRouter, HTTPException from fastapi_sqlalchemy import db -from .models.category import CategoryCreate, CategoryUpdate, CategoryGet -from ..models.database import Category, Button +from ..models.database import Button, Category +from .models.category import CategoryCreate, CategoryGet, CategoryUpdate + category = APIRouter() diff --git a/services_backend/settings.py b/services_backend/settings.py index 234f2d8..a602307 100644 --- a/services_backend/settings.py +++ b/services_backend/settings.py @@ -7,7 +7,7 @@ class Settings(BaseSettings): """Application settings""" - DB_DSN: PostgresDsn + DB_DSN: PostgresDsn = 'postgresql://postgres@localhost:5432/postgres' ROOT_PATH: str = '/' + os.getenv('APP_NAME', '') CORS_ALLOW_ORIGINS: list[str] = ['*'] From d61e318b8ae66536c685b93bda316cd6133310f9 Mon Sep 17 00:00:00 2001 From: Dyakov Roman Date: Thu, 16 Mar 2023 13:43:27 +0300 Subject: [PATCH 2/5] Auth --- requirements.txt | 1 + services_backend/routes/button.py | 22 ++++++++++++++++++---- services_backend/routes/category.py | 19 +++++++++++++++---- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0be3865..653a90e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ logging-profcomff autoflake isort black +auth-lib-profcomff[fastapi] diff --git a/services_backend/routes/button.py b/services_backend/routes/button.py index c73c606..f24f3fd 100644 --- a/services_backend/routes/button.py +++ b/services_backend/routes/button.py @@ -1,4 +1,5 @@ -from fastapi import APIRouter, HTTPException +from auth_lib.fastapi import UnionAuth +from fastapi import APIRouter, Depends, HTTPException from fastapi_sqlalchemy import db from ..models.database import Button, Category @@ -10,7 +11,11 @@ @button.post("/", response_model=ButtonGet) -def create_button(button_inp: ButtonCreate, category_id: int): +def create_button( + button_inp: ButtonCreate, + category_id: int, + user=Depends(UnionAuth(['services.button.create'])), +): category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -46,7 +51,11 @@ def get_button(button_id: int, category_id: int): @button.delete("/{button_id}", response_model=None) -def remove_button(button_id: int, category_id: int): +def remove_button( + button_id: int, + category_id: int, + user=Depends(UnionAuth(['services.button.remove'])), +): category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -61,7 +70,12 @@ def remove_button(button_id: int, category_id: int): @button.patch("/{button_id}", response_model=ButtonUpdate) -def update_button(button_inp: ButtonUpdate, button_id: int, category_id: int): +def update_button( + button_inp: ButtonUpdate, + button_id: int, + category_id: int, + user=Depends(UnionAuth(['services.button.update'])), +): query = db.session.query(Button).filter(Button.id == button_id) button = query.one_or_none() last_button = ( diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index bf7a32b..9814342 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -1,4 +1,5 @@ -from fastapi import APIRouter, HTTPException +from auth_lib.fastapi import UnionAuth +from fastapi import APIRouter, Depends, HTTPException from fastapi_sqlalchemy import db from ..models.database import Button, Category @@ -9,7 +10,10 @@ @category.post("/", response_model=CategoryGet) -def create_category(category_inp: CategoryCreate): +def create_category( + category_inp: CategoryCreate, + user=Depends(UnionAuth(['services.category.create'])), +): last_category = db.session.query(Category).order_by(Category.order.desc()).first() category = Category(**category_inp.dict(exclude_none=True)) if last_category: @@ -43,7 +47,10 @@ def get_category(category_id: int): @category.delete("/{category_id}", response_model=None) -def remove_category(category_id: int): +def remove_category( + category_id: int, + user=Depends(UnionAuth(['services.category.delete'])), +): category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -56,7 +63,11 @@ def remove_category(category_id: int): @category.patch("/{category_id}", response_model=CategoryUpdate) -def update_category(category_inp: CategoryUpdate, category_id: int): +def update_category( + category_inp: CategoryUpdate, + category_id: int, + user=Depends(UnionAuth(['services.category.update'])), +): category = db.session.query(Category).filter(Category.id == category_id).one_or_none() last_category = db.session.query(Category).order_by(Category.order.desc()).first() From 975fdabc8706de7ab218a75d66e606b6198a383d Mon Sep 17 00:00:00 2001 From: Dyakov Roman Date: Thu, 16 Mar 2023 14:03:07 +0300 Subject: [PATCH 3/5] Auth add --- .github/workflows/build_and_publish.yml | 2 ++ services_backend/routes/button.py | 39 ++++++++++++++++++-- services_backend/routes/category.py | 47 +++++++++++++++++++++---- 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build_and_publish.yml b/.github/workflows/build_and_publish.yml index b67b315..8fa453d 100644 --- a/.github/workflows/build_and_publish.yml +++ b/.github/workflows/build_and_publish.yml @@ -87,6 +87,7 @@ jobs: --network=web \ --env DB_DSN='${{ secrets.DB_DSN }}' \ --env ROOT_PATH='/services' \ + --env AUTH_URL='https://api.test.profcomff.com/auth' \ --env GUNICORN_CMD_ARGS='--log-config logging_test.conf' \ --name ${{ env.CONTAITER_NAME }} \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:test @@ -129,6 +130,7 @@ jobs: --network=web \ --env DB_DSN='${{ secrets.DB_DSN }}' \ --env ROOT_PATH='/services' \ + --env AUTH_URL='https://api.profcomff.com/auth' \ --env GUNICORN_CMD_ARGS='--log-config logging_prod.conf' \ --name ${{ env.CONTAITER_NAME }} \ ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest diff --git a/services_backend/routes/button.py b/services_backend/routes/button.py index f24f3fd..19429b6 100644 --- a/services_backend/routes/button.py +++ b/services_backend/routes/button.py @@ -1,3 +1,5 @@ +import logging + from auth_lib.fastapi import UnionAuth from fastapi import APIRouter, Depends, HTTPException from fastapi_sqlalchemy import db @@ -7,6 +9,7 @@ from .models.category import CategoryGet +logger = logging.getLogger(__name__) button = APIRouter() @@ -16,6 +19,11 @@ def create_button( category_id: int, user=Depends(UnionAuth(['services.button.create'])), ): + """Создать кнопку + + Необходимые scopes: `services.button.create` + """ + logger.info(f"User {user} triggered create_button") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -30,7 +38,15 @@ def create_button( @button.get("/", response_model=CategoryGet) -def get_buttons(category_id: int): +def get_buttons( + category_id: int, + user=Depends(UnionAuth(allow_none=True, auto_error=False)), +): + """Показать все кнопки в категории + + Необходимые scopes: `-` + """ + logger.info(f"User {user} triggered create_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -38,7 +54,16 @@ def get_buttons(category_id: int): @button.get("/{button_id}", response_model=ButtonGet) -def get_button(button_id: int, category_id: int): +def get_button( + button_id: int, + category_id: int, + user=Depends(UnionAuth(allow_none=True, auto_error=False)), +): + """Показать одну кнопку + + Необходимые scopes: `-` + """ + logger.info(f"User {user} triggered create_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -56,6 +81,11 @@ def remove_button( category_id: int, user=Depends(UnionAuth(['services.button.remove'])), ): + """Удалить кнопку + + Необходимые scopes: `services.button.remove` + """ + logger.info(f"User {user} triggered create_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -76,6 +106,11 @@ def update_button( category_id: int, user=Depends(UnionAuth(['services.button.update'])), ): + """Обновить кнопку + + Необходимые scopes: `services.button.update` + """ + logger.info(f"User {user} triggered create_category") query = db.session.query(Button).filter(Button.id == button_id) button = query.one_or_none() last_button = ( diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index 9814342..1339d05 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -1,11 +1,15 @@ +import logging +from typing import Literal + from auth_lib.fastapi import UnionAuth -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query from fastapi_sqlalchemy import db from ..models.database import Button, Category from .models.category import CategoryCreate, CategoryGet, CategoryUpdate +logger = logging.getLogger(__name__) category = APIRouter() @@ -14,6 +18,11 @@ def create_category( category_inp: CategoryCreate, user=Depends(UnionAuth(['services.category.create'])), ): + """Создает категорию + + Необходимые scopes: `services.category.create` + """ + logger.info(f"User {user} triggered create_category") last_category = db.session.query(Category).order_by(Category.order.desc()).first() category = Category(**category_inp.dict(exclude_none=True)) if last_category: @@ -24,17 +33,31 @@ def create_category( @category.get("/", response_model=list[CategoryGet], response_model_exclude_none=True) -def get_categories(offset: int = 0, limit: int = 100): - if (offset < 0) or (limit < 0): - raise HTTPException(400, detail="Offset or limit cant be negative") +def get_categories( + info: list[Literal['buttons']] = Query([]), + user=Depends(UnionAuth(allow_none=True, auto_error=False)), +): + """Показывает список категорий + + Необходимые scopes: `-` + """ + logger.info(f"User {user} triggered get_categories") return [ - CategoryGet.from_orm(row).dict(exclude={"buttons"}) - for row in db.session.query(Category).order_by(Category.order).offset(offset).limit(limit).all() + CategoryGet.from_orm(row).dict(exclude={"buttons"} if 'buttons' not in info else {}) + for row in db.session.query(Category).order_by(Category.order).all() ] @category.get("/{category_id}", response_model=CategoryGet, response_model_exclude_none=True) -def get_category(category_id: int): +def get_category( + category_id: int, + user=Depends(UnionAuth(allow_none=True, auto_error=False)), +): + """Показывает категорию + + Необходимые scopes: `-` + """ + logger.info(f"User {user} triggered get_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -51,6 +74,11 @@ def remove_category( category_id: int, user=Depends(UnionAuth(['services.category.delete'])), ): + """Удаляет категорию и все кнопки в ней + + Необходимые scopes: `services.category.delete` + """ + logger.info(f"User {user} triggered remove_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -68,6 +96,11 @@ def update_category( category_id: int, user=Depends(UnionAuth(['services.category.update'])), ): + """Обновляет категорию + + Необходимые scopes: `services.category.update` + """ + logger.info(f"User {user} triggered update_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() last_category = db.session.query(Category).order_by(Category.order.desc()).first() From 25cdc60c46454e77c2dc1b692cab5c0f821ad1c7 Mon Sep 17 00:00:00 2001 From: Dyakov Roman Date: Thu, 16 Mar 2023 14:10:55 +0300 Subject: [PATCH 4/5] Log fix --- services_backend/routes/button.py | 10 +++++----- services_backend/routes/category.py | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/services_backend/routes/button.py b/services_backend/routes/button.py index 19429b6..d7dc535 100644 --- a/services_backend/routes/button.py +++ b/services_backend/routes/button.py @@ -23,7 +23,7 @@ def create_button( Необходимые scopes: `services.button.create` """ - logger.info(f"User {user} triggered create_button") + logger.info(f"User {user.get('id')} triggered create_button") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -46,7 +46,7 @@ def get_buttons( Необходимые scopes: `-` """ - logger.info(f"User {user} triggered create_category") + logger.info(f"User {user.get('id')} triggered create_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -63,7 +63,7 @@ def get_button( Необходимые scopes: `-` """ - logger.info(f"User {user} triggered create_category") + logger.info(f"User {user.get('id')} triggered create_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -85,7 +85,7 @@ def remove_button( Необходимые scopes: `services.button.remove` """ - logger.info(f"User {user} triggered create_category") + logger.info(f"User {user.get('id')} triggered create_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -110,7 +110,7 @@ def update_button( Необходимые scopes: `services.button.update` """ - logger.info(f"User {user} triggered create_category") + logger.info(f"User {user.get('id')} triggered create_category") query = db.session.query(Button).filter(Button.id == button_id) button = query.one_or_none() last_button = ( diff --git a/services_backend/routes/category.py b/services_backend/routes/category.py index 1339d05..b81d55b 100644 --- a/services_backend/routes/category.py +++ b/services_backend/routes/category.py @@ -22,7 +22,7 @@ def create_category( Необходимые scopes: `services.category.create` """ - logger.info(f"User {user} triggered create_category") + logger.info(f"User {user.get('id')} triggered create_category") last_category = db.session.query(Category).order_by(Category.order.desc()).first() category = Category(**category_inp.dict(exclude_none=True)) if last_category: @@ -41,7 +41,7 @@ def get_categories( Необходимые scopes: `-` """ - logger.info(f"User {user} triggered get_categories") + logger.info(f"User {user.get('id')} triggered get_categories") return [ CategoryGet.from_orm(row).dict(exclude={"buttons"} if 'buttons' not in info else {}) for row in db.session.query(Category).order_by(Category.order).all() @@ -57,7 +57,7 @@ def get_category( Необходимые scopes: `-` """ - logger.info(f"User {user} triggered get_category") + logger.info(f"User {user.get('id')} triggered get_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -78,7 +78,7 @@ def remove_category( Необходимые scopes: `services.category.delete` """ - logger.info(f"User {user} triggered remove_category") + logger.info(f"User {user.get('id')} triggered remove_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() if not category: raise HTTPException(status_code=404, detail="Category does not exist") @@ -100,7 +100,7 @@ def update_category( Необходимые scopes: `services.category.update` """ - logger.info(f"User {user} triggered update_category") + logger.info(f"User {user.get('id')} triggered update_category") category = db.session.query(Category).filter(Category.id == category_id).one_or_none() last_category = db.session.query(Category).order_by(Category.order.desc()).first() From 17aa5b0d9fc882a95470e38522dddd14029646f1 Mon Sep 17 00:00:00 2001 From: Dyakov Roman Date: Thu, 16 Mar 2023 14:24:32 +0300 Subject: [PATCH 5/5] Tests fix --- requirements.dev.txt | 6 +++++- requirements.txt | 3 --- tests/conftest.py | 19 +++++++++++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/requirements.dev.txt b/requirements.dev.txt index 2b10fc2..661f027 100644 --- a/requirements.dev.txt +++ b/requirements.dev.txt @@ -1,3 +1,7 @@ pytest pytest-cov -httpx \ No newline at end of file +httpx +pytest_mock +autoflake +isort +black diff --git a/requirements.txt b/requirements.txt index 653a90e..e72fced 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,4 @@ alembic SQLAlchemy gunicorn logging-profcomff -autoflake -isort -black auth-lib-profcomff[fastapi] diff --git a/tests/conftest.py b/tests/conftest.py index d967a94..5e1b17c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,14 +1,25 @@ import pytest from fastapi.testclient import TestClient -from sqlalchemy.orm import Session, sessionmaker +from pytest_mock import MockerFixture from sqlalchemy import create_engine +from sqlalchemy.orm import Session, sessionmaker + +from services_backend.models.base import Base from services_backend.routes.base import app from services_backend.settings import get_settings -from services_backend.models.base import Base -@pytest.fixture(scope='session') -def client(): +@pytest.fixture() +def client(mocker: MockerFixture): + user_mock = mocker.patch('auth_lib.fastapi.UnionAuth.__call__') + user_mock.return_value = { + "session_scopes": [{"id": 0, "name": "string", "comment": "string"}], + "user_scopes": [{"id": 0, "name": "string", "comment": "string"}], + "indirect_groups": [{"id": 0, "name": "string", "parent_id": 0}], + "groups": [{"id": 0, "name": "string", "parent_id": 0}], + "id": 0, + "email": "string", + } client = TestClient(app) return client