From 6009802cd3b3b55adf229b9570182bfd8dc806b6 Mon Sep 17 00:00:00 2001 From: flavien-hugs Date: Sun, 4 Aug 2024 16:03:58 +0000 Subject: [PATCH] Add bug and fix bug --- .coveragerc | 6 ++ pyproject.toml | 44 +++++++++++++- src/__init__.py | 1 + src/routers/users.py | 2 +- src/services/roles.py | 2 +- src/services/users.py | 12 ++-- tests/config/__init__.py | 0 tests/config/test_db_config.py | 27 +++++++++ tests/conftest.py | 101 ++++++++++++++++++++++++++++++++ tests/routers/__init__.py | 0 tests/routers/test_users_api.py | 63 ++++++++++++++++++++ tests/test.env | 38 ++++++++++++ 12 files changed, 286 insertions(+), 10 deletions(-) create mode 100644 .coveragerc create mode 100644 tests/config/__init__.py create mode 100644 tests/config/test_db_config.py create mode 100644 tests/conftest.py create mode 100644 tests/routers/__init__.py create mode 100644 tests/routers/test_users_api.py create mode 100644 tests/test.env diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..e8a89d3 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,6 @@ +[run] +include = + */src/* +omit = + */tests/* + */src/common/* diff --git a/pyproject.toml b/pyproject.toml index e34b0c4..ab1f061 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,11 +14,19 @@ pwdlib = {extras = ["argon2", "bcrypt"], version = "^0.2.0"} pydantic-settings = "^2.4.0" fastapi-jwt = "^0.3.0" python-jose = "^3.3.0" -fastapi = {extras = ["standard"], version = "^0.112.0"} +fastapi = {extras = ["all"], version = "^0.112.0"} [tool.poetry.group.test.dependencies] -python-dotenv = "^1.0.1" +pytest = "^8.3.2" +coverage = "^7.6.0" +pytest-mock = "^3.14.0" +pytest-asyncio = "^0.23.8" +mongomock-motor = "^0.0.31" +faker = "^26.1.0" +setuptools = "^72.1.0" +pytest-cov = "^5.0.0" +pytest-dotenv = "^0.5.2" [tool.poetry.group.dev.dependencies] @@ -31,3 +39,35 @@ app = 'src.cli:app' [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 120 +include = '\.pyi?$' +exclude = ''' +( + /( + \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.tox + | \.venv + | \.docker + | \*.egg-info + | _build + | buck-out + | build + | dist + )/ +) +''' + +[tool.pytest.ini_options] +env_files = 'tests/test.env' +env_override_existing_values = 1 +python_files = "test_*.py" +asyncio_mode = "auto" +filterwarnings = [ + "ignore", + "ignore:.*U.*mode is deprecated:DeprecationWarning" +] diff --git a/src/__init__.py b/src/__init__.py index d4bb4ed..2562dd3 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -38,6 +38,7 @@ async def lifespan(app: FastAPI): summary=f"{settings.APP_TITLE}", docs_url=f"/{BASE_URL}/docs", openapi_url=f"/{BASE_URL}/openapi.json", + root_path_in_servers=False, ) app.include_router(auth_router) diff --git a/src/routers/users.py b/src/routers/users.py index 5215d7c..1125750 100644 --- a/src/routers/users.py +++ b/src/routers/users.py @@ -39,7 +39,7 @@ async def create_user(payload: CreateUser = Body(...)): async def listing_users( query: Optional[str] = Query(None, description="Filter by user"), # is_primary: bool = Query(default=False, description="Filter grant super admin"), - is_active: bool = Query(default=False, description="Filter account is active or disable"), + is_active: bool = Query(default=True, description="Filter account is active or disable"), sorting: Optional[SortEnum] = Query(SortEnum.DESC, description="Order by creation date: 'asc' or 'desc"), ): # search = {"is_primary": is_primary} diff --git a/src/services/roles.py b/src/services/roles.py index 29b3534..f8cc6b3 100644 --- a/src/services/roles.py +++ b/src/services/roles.py @@ -121,7 +121,7 @@ async def delete_role(role_id: PydanticObjectId) -> None: :return: :rtype: """ - await Role.get(document_id=PydanticObjectId(role_id)).delete() + await Role.find_one({"_id": PydanticObjectId(role_id)}).delete() async def delete_many_roles(role_ids: Sequence[PydanticObjectId]) -> None: diff --git a/src/services/users.py b/src/services/users.py index 0632497..c3befa3 100644 --- a/src/services/users.py +++ b/src/services/users.py @@ -20,16 +20,16 @@ async def create_user(user: CreateUser) -> User: """ Créer un nouvel utilisateur - :param user: - :type user: - :return: - :rtype: + :param user: Les informations de l'utilisateur à créer + :type user: CreateUser + :return: Le nouvel utilisateur créé + :rtype: User """ await get_one_role(role_id=PydanticObjectId(user.role)) - if User.find_one({"email": user.email.lower(), "is_active": True}).exists() is True: + if await User.find_one({"email": user.email, "is_active": True}).exists(): raise CustomHTTException( code_error=UserErrorCode.USER_EMAIL_ALREADY_EXIST, - message_error=f"User with email '{user.email.lower()}' alreay exits", + message_error=f"User with email '{user.email.lower()}' already exists", status_code=status.HTTP_400_BAD_REQUEST, ) diff --git a/tests/config/__init__.py b/tests/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/config/test_db_config.py b/tests/config/test_db_config.py new file mode 100644 index 0000000..0d4815e --- /dev/null +++ b/tests/config/test_db_config.py @@ -0,0 +1,27 @@ +from unittest import mock + +import pytest + +from src.config import settings, shutdown_db, startup_db + + +@pytest.mark.asyncio +@mock.patch("src.config.settings") +@mock.patch("src.config.database.init_beanie", return_value=None) +async def test_startup_db( + mock_settings, mock_init_beanie, mock_mongodb_client, mock_app_instance, fixture_models +): + mock_settings.return_value = mock.Mock(MONGODB_URI=settings.MONGODB_URI, DB_NAME=settings.MONGODB_URI) + + await startup_db(app=mock_app_instance, models=[fixture_models.User, fixture_models.Role]) + + mock_settings.assert_called_once() + assert mock_app_instance.mongo_db_client is not None + assert mock_mongodb_client.is_mongos is True + + +@pytest.mark.asyncio +async def test_shutdown_db(mock_app_instance): + mock_app_instance.mongo_db_client = mock.AsyncMock() + await shutdown_db(app=mock_app_instance) + mock_app_instance.mongo_db_client.close.assert_called_once() diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ee518ef --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,101 @@ +from unittest import mock + +import faker +import pytest +import pytest_asyncio +from beanie import init_beanie +from httpx import AsyncClient +from mongomock_motor import AsyncMongoMockClient + +from src.config import settings + + +@pytest.fixture +def fake_data(): + return faker.Faker() + + +@pytest.fixture() +def fixture_models(): + from src import models + + return models + + +@pytest.fixture +async def mock_app_instance(): + from src import app as mock_app + + yield mock_app + + +@pytest.fixture(autouse=True) +async def mock_mongodb_client(mock_app_instance, fixture_models): + client = AsyncMongoMockClient() + mock_app_instance.mongo_db_client = client[settings.MONGO_DB] + await init_beanie( + database=mock_app_instance.mongo_db_client, + document_models=[fixture_models.User, fixture_models.Role], + ) + yield client + + +@pytest.fixture(autouse=True) +async def clean_db(fixture_models, mock_mongodb_client): + for model in [fixture_models.User, fixture_models.Role]: + await model.delete_all() + + +@pytest.fixture() +def mock_auth_token_bearer(): + with mock.patch("src.routers.users.AuthorizeHTTPBearer.__call__", return_value=True): + yield + + +@pytest.fixture() +def mock_chek_permissions_handler(): + with mock.patch( + "src.routers.users.CheckPermissionsHandler.__call__", return_value=True + ): + yield + + +@pytest.fixture(autouse=True) +async def http_client_api(mock_app_instance, clean_db): + async with AsyncClient( + app=mock_app_instance, + base_url="http://auth.localhost.com", + follow_redirects=True, + ) as client: + yield client + + +@pytest.fixture() +def fake_role_data(fake_data): + return { + "name": fake_data.unique.name(), + "description": fake_data.text(), + } + + +@pytest_asyncio.fixture() +async def fake_role_collection(fixture_models, fake_role_data): + result = await fixture_models.Role(**fake_role_data).create() + return result + + +@pytest.fixture() +def fake_user_data(fake_role_collection, fake_data): + return { + "email": fake_data.unique.email().lower(), + "fullname": fake_data.name(), + "role": str(fake_role_collection.id), + "attributes": {"city": fake_data.city()}, + "password": fake_data.password() + } + + +@pytest_asyncio.fixture() +async def fake_user_collection(fixture_models, fake_user_data): + result = await fixture_models.User(**fake_user_data).create() + return result diff --git a/tests/routers/__init__.py b/tests/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/routers/test_users_api.py b/tests/routers/test_users_api.py new file mode 100644 index 0000000..d300fd0 --- /dev/null +++ b/tests/routers/test_users_api.py @@ -0,0 +1,63 @@ +import faker +import pytest +from starlette import status + +from src.shared.error_codes import UserErrorCode, RoleErrorCode, AuthErrorCode +from src.common.helpers.error_codes import AppErrorCode +# fake = faker.Faker() + + +@pytest.mark.asyncio +async def test_create_users_success(http_client_api, fake_user_data): + response = await http_client_api.post("/users", json=fake_user_data) + + response_json = response.json() + assert response.status_code == status.HTTP_201_CREATED, response.text + assert "_id" in response_json.keys() + assert response_json["email"] == fake_user_data["email"] + + +@pytest.mark.asyncio +async def test_create_user_already_exists(http_client_api, fake_user_collection, fake_user_data): + fake_user_data.update({"email": fake_user_collection.email}) + + response = await http_client_api.post("/users", json=fake_user_data) + + response_json = response.json() + assert response.status_code == status.HTTP_400_BAD_REQUEST, response.text + assert response_json["code_error"] == UserErrorCode.USER_EMAIL_ALREADY_EXIST + assert response_json["message_error"] == f"User with email '{fake_user_data['email'].lower()}' already exists" + + +@pytest.mark.asyncio +async def test_create_user_with_email_empty(http_client_api, fake_user_data): + fake_user_data.update({"email": None}) + + response = await http_client_api.post("/users", json=fake_user_data) + + response_json = response.json() + assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY, response.text + assert response_json["code_error"] == AppErrorCode.REQUEST_VALIDATION_ERROR + assert response_json["message_error"] == "[{'field': 'email', 'message': 'Input should be a valid string'}]" + + +@pytest.mark.asyncio +async def test_create_user_with_role_empty(http_client_api, fake_user_data): + fake_user_data.update({"role": None}) + response = await http_client_api.post("/users", json=fake_user_data) + response_json = response.json() + + assert response.status_code == status.HTTP_400_BAD_REQUEST, response.text + assert response_json["code_error"] == RoleErrorCode.ROLE_NOT_FOUND + + +@pytest.mark.asyncio +async def test_create_user_with_password_empty(http_client_api, fake_user_data): + fake_user_data.update({"password": ""}) + + response = await http_client_api.post("/users", json=fake_user_data) + + response_json = response.json() + assert response.status_code == status.HTTP_400_BAD_REQUEST, response.text + assert response_json["code_error"] == AuthErrorCode.AUTH_PASSWORD_MISMATCH + assert response_json["message_error"] == "The password must be 6 characters or more." diff --git a/tests/test.env b/tests/test.env new file mode 100644 index 0000000..0a8f7d7 --- /dev/null +++ b/tests/test.env @@ -0,0 +1,38 @@ +# APP CONFIG +APP_NAME="auth-test" +APP_TITLE="Authentication and user management system" +APP_HOSTNAME="0.0.0.0" +APP_RELOAD=1 +APP_ACCESS_LOG=1 +FULLNAME_MIN_LENGTH=4 +PASSWORD_MIN_LENGTH=6 +APP_DEFAULT_PORT=9077 +DEFAULT_PAGIGNIATE_PAGE_SIZE=10 + +DEFAULT_ADMIN_FULLNAME="Admin HAF" +DEFAULT_ADMIN_EMAIL=test@localhost.com +DEFAULT_ADMIN_PASSWORD=password +DEFAULT_ADMIN_ROLE=super-admin-test +DEFAULT_ADMIN_ROLE_DESCRIPTION="Super administrateur" + +# USER MODEL NAME +USER_MODEL_NAME=test_auth_coll +ROLE_MODEL_NAME=test_roles_coll + +# MONGODB URI CONFIG +MONGO_DB=test +APP_DESC_DB_COLLECTION=${MONGO_DB}.appdesc +PERMS_DB_COLLECTION=${MONGO_DB}.permissions +MONGODB_URI=mongodb://user:password@localhost:27017 + +# SMTP CONFIG +SMTP_PORT=587 +EMAIL_PASSWORD="1234567" +SMTP_SERVER="smtp.gmail.com" +EMAIL_ADDRESS="test@gmail.com" + +# JWT CONFIG +JWT_SECRET_KEY=ddee7aba9ce88dd6 +JWT_ALGORITHM="HS256" +ACCESS_TOKEN_EXPIRE_MINUTES=3600 +REFRESH_TOKEN_EXPIRE_MINUTES=720