Skip to content

Commit

Permalink
Merge pull request #1 from flavien-hugs/develop
Browse files Browse the repository at this point in the history
Develop
  • Loading branch information
flavien-hugs authored Aug 4, 2024
2 parents 16d138b + 6009802 commit 497eedc
Show file tree
Hide file tree
Showing 13 changed files with 289 additions and 10 deletions.
6 changes: 6 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[run]
include =
*/src/*
omit =
*/tests/*
*/src/common/*
3 changes: 3 additions & 0 deletions .github/workflows/publish-ghrch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: true
token: ${{ secrets.GH_SUBMODULE_TOKEN }}

- name: Login to Docker registry
run: |
Expand Down
44 changes: 42 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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"
]
1 change: 1 addition & 0 deletions src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src/routers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion src/services/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
12 changes: 6 additions & 6 deletions src/services/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)

Expand Down
Empty file added tests/config/__init__.py
Empty file.
27 changes: 27 additions & 0 deletions tests/config/test_db_config.py
Original file line number Diff line number Diff line change
@@ -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()
101 changes: 101 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -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
Empty file added tests/routers/__init__.py
Empty file.
63 changes: 63 additions & 0 deletions tests/routers/test_users_api.py
Original file line number Diff line number Diff line change
@@ -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."
38 changes: 38 additions & 0 deletions tests/test.env
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 497eedc

Please sign in to comment.