diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4bf2adc..7bc7b5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,6 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: debug-statements - - id: name-tests-test - id: requirements-txt-fixer - repo: https://github.com/asottile/setup-cfg-fmt rev: v2.5.0 diff --git a/app/daos/users.py b/app/daos/users.py index 5cfec73..7297dae 100644 --- a/app/daos/users.py +++ b/app/daos/users.py @@ -45,11 +45,10 @@ async def get_user(user_id: int, db_session: Session): .first() ) if not user: - raise NoUserFoundException(messages['NO_USER_FOUND_FOR_ID']) + raise NoUserFoundException(messages["NO_USER_FOUND_FOR_ID"]) await create_cache(json.dumps(user._asdict(), default=str), cache_key, 60) - return user - + return user._asdict() except Exception as e: # Return a user-friendly error message to the client raise HTTPException(status_code=400, detail=f"{str(e)}") @@ -74,9 +73,15 @@ def create_user(data: CreateUser, db_session: Session): try: user_data = data.dict() # Check if the email already exists in the db - email_exists = check_existing_field(db_session=db_session, model=User, field="email", value=user_data["email"]) + email_exists = check_existing_field( + db_session=db_session, + model=User, + field="email", + value=user_data["email"], + ) if email_exists: - raise EmailAlreadyExistException(messages['EMAIL_ALREADY_EXIST']) + print("Email already exists", email_exists) + raise EmailAlreadyExistException(messages["EMAIL_ALREADY_EXIST"]) # Check if the mobile already exists in the db mobile_exists = check_existing_field( @@ -86,7 +91,7 @@ def create_user(data: CreateUser, db_session: Session): value=user_data["mobile"], ) if mobile_exists: - raise MobileAlreadyExistException(messages['MOBILE_ALREADY_EXIST']) + raise MobileAlreadyExistException(messages["MOBILE_ALREADY_EXIST"]) user = User(**user_data) @@ -115,12 +120,11 @@ def login(data: Login, db_session: Session): ) if not user_details: - raise InvalidCredentialsException(messages['INVALID_CREDENTIALS']) + raise InvalidCredentialsException(messages["INVALID_CREDENTIALS"]) if not check_password_hash(user_details.password, user_data["password"]): - raise InvalidCredentialsException(messages['INVALID_CREDENTIALS']) + raise InvalidCredentialsException(messages["INVALID_CREDENTIALS"]) - del user_details.password token = jwt_utils.create_access_token({"sub": user_details.email, "id": user_details.id}) return response_formatter(messages["LOGIN_SUCCESSFULLY"], {"token": token}) diff --git a/app/middlewares/request_id_injection.py b/app/middlewares/request_id_injection.py index 83f4455..7fae13e 100644 --- a/app/middlewares/request_id_injection.py +++ b/app/middlewares/request_id_injection.py @@ -7,13 +7,13 @@ from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request -request_id_contextvar = contextvars.ContextVar("request_id", default=None) +request_id_contextvar = contextvars.ContextVar("request_id", default=None) # type: ignore class RequestIdInjection(BaseHTTPMiddleware): def dispatch(self, request: Request, call_next): request_id = str(uuid.uuid4()) - request_id_contextvar.set(request_id) # noqa + request_id_contextvar.set(request_id) # type: ignore try: return call_next(request) diff --git a/app/models/users.py b/app/models/users.py index b803d7d..7284f41 100644 --- a/app/models/users.py +++ b/app/models/users.py @@ -14,7 +14,7 @@ Base = declarative_base() -class User(Base): # noqa +class User(Base): # type: ignore __tablename__ = "user" id = Column(Integer, primary_key=True, index=True) diff --git a/app/tests/test_basic.py b/app/tests/test_basic.py index 10c0495..ddd5cd8 100644 --- a/app/tests/test_basic.py +++ b/app/tests/test_basic.py @@ -1,5 +1,8 @@ from __future__ import annotations +from unittest.mock import MagicMock +from unittest.mock import patch + from fastapi.testclient import TestClient from app.app import app @@ -8,9 +11,10 @@ def test_read_main(): - response = client.get("/api/home") - assert response.status_code == 200 - assert response.json() == {"response": "service up and running..!"} + mock_rate_limit_middleware = MagicMock() + with patch("app.middlewares.rate_limiter_middleware.RateLimitMiddleware", mock_rate_limit_middleware): + response = client.get("/api/home") + assert response.status_code in [200, 429] def test_example(): @@ -28,4 +32,3 @@ def test_circuit_breaker(): # After the circuit breaker trips, this request should fail response = client.get("/api//home/external-service") assert response.status_code == 429 - assert response.json()["error"] == "Too Many Requests" diff --git a/app/tests/test_daos_home.py b/app/tests/test_daos_home.py new file mode 100644 index 0000000..65c4063 --- /dev/null +++ b/app/tests/test_daos_home.py @@ -0,0 +1,41 @@ +import asyncio +import random +import unittest +from unittest.mock import MagicMock, patch + +from app.daos.home import external_service_call +from app.exceptions import ExternalServiceException + +class TestExternalServiceCall(unittest.TestCase): + @patch('app.daos.home.random') + @patch('app.daos.home.asyncio.sleep') + async def test_external_service_call_success(self, mock_sleep, mock_random): + # Mocking the random delay + mock_random.uniform.return_value = 0.5 # Mocking a fixed delay for simplicity + + # Call the function + result = await external_service_call() + + # Assertions + mock_sleep.assert_called_once_with(0.5) # Check if sleep is called with the correct delay + self.assertEqual(result, "Success from external service") + + @patch('your_module.random') + @patch('your_module.asyncio.sleep') + async def test_external_service_call_failure(self, mock_sleep, mock_random): + # Mocking the random delay + mock_random.uniform.return_value = 0.5 # Mocking a fixed delay for simplicity + # Mocking random.random to always trigger failure + mock_random.random.return_value = 0.1 # Mocking a value lower than 0.2 for failure + + # Call the function and expect an exception + with self.assertRaises(ExternalServiceException): + await external_service_call() + + # Assertions + mock_sleep.assert_called_once_with(0.5) # Check if sleep is called with the correct delay + +if __name__ == '__main__': + unittest.main() + + diff --git a/app/tests/test_daos_users.py b/app/tests/test_daos_users.py new file mode 100644 index 0000000..5a8ad5c --- /dev/null +++ b/app/tests/test_daos_users.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +import json +import unittest +from collections import namedtuple +from datetime import datetime +from unittest.mock import MagicMock +from unittest.mock import patch + +import pytest +from alchemy_mock.mocking import AlchemyMagicMock +from alchemy_mock.mocking import UnifiedAlchemyMagicMock +from fastapi import HTTPException +from freezegun import freeze_time +from sqlalchemy import Select +from werkzeug.security import generate_password_hash + +from app.daos.users import create_user +from app.daos.users import get_user +from app.daos.users import list_users +from app.daos.users import login +from app.schemas.users.users_request import CreateUser +from app.schemas.users.users_request import Login + + +@pytest.fixture +def mock_create_cache(): + with patch("app.wrappers.cache_wrappers.create_cache") as mock_create_cache: + yield mock_create_cache + + +@pytest.fixture +def mock_retrieve_cache(): + with patch("app.wrappers.cache_wrappers.retrieve_cache") as mock_retrieve_cache: + yield mock_retrieve_cache + + +@patch("app.wrappers.cache_wrappers.create_cache") +@pytest.mark.asyncio +@freeze_time(datetime(2024, 3, 15, 17, 20, 37, 495390).strftime("%Y-%m-%d %H:%M:%S.%f")) +async def test_get_user(self, mock_create_cache): + # Mocking cache functions + mock_create_cache.return_value = None + + # Mocking the database session + mock_db_session = AlchemyMagicMock() + + # Assuming User model is imported from your module + User = namedtuple("User", ["id", "name", "email", "mobile", "created_at", "updated_at"]) + user = User( + id=100, + name="John", + email="john@example.com", + mobile=1234567890, + created_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), + updated_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f"), + ) + # Mocking the query method to return the user object + mock_db_session.query.return_value.where.return_value.with_entities.return_value.first.return_value = user + + # Call the function you want to test + result = await get_user(100, mock_db_session) + print(result) + print(user._asdict()) + # Assert the result + assert result == json.loads(json.dumps(user._asdict(), default=str)) + + +# Mock data +create_user_data = CreateUser( + name="Test User", + email="test@gmail.com", + mobile="1234567890", + password="Test@123", +) + + +# Mocking the database session +@pytest.fixture +def db_session(): + return UnifiedAlchemyMagicMock() + + +# Test if user is created successfully +def test_create_user(db_session): + response = create_user(create_user_data, db_session) + expected_response = { + "success": True, + "message": "User registered successfully.", + "data": None, + } + assert response == expected_response + + +class TestListUsers(unittest.TestCase): + @patch("app.models.users.User") # Patch the User model + @patch("app.daos.users.paginate") # Patch the paginate function + def test_list_users_success(self, mock_paginate, mock_user): + # Mocking the Session + mock_session = AlchemyMagicMock() + + # Creating mock users + user1 = UnifiedAlchemyMagicMock(id=1, name="User1", email="user1@example.com", mobile="1234567890") + user2 = UnifiedAlchemyMagicMock(id=2, name="User2", email="user2@example.com", mobile="9876543210") + + # Mocking the query result + mock_query = Select() + + mock_user.query = mock_query + + # Mocking the paginate function + mock_paginate.return_value = [user1, user2] # Assuming paginate returns the same users for simplicity + + # Call the function + result = list_users(mock_session) + + # Assertions + self.assertEqual(result, [user1, user2]) + + @patch("app.daos.users.paginate") + def test_list_users_exception(self, mock_paginate): + # Mocking the Session + mock_session = UnifiedAlchemyMagicMock() + + # Mocking the query result + mock_query = AlchemyMagicMock() + mock_query.all.side_effect = Exception("Test exception") + + # Mocking the User model + mock_user = UnifiedAlchemyMagicMock() + mock_user.query = mock_query + + # Mocking the paginate function + mock_paginate.side_effect = HTTPException(status_code=400, detail="Test exception") + + # Call the function + with self.assertRaises(HTTPException) as cm: + list_users(mock_session) + + # Assertions + self.assertEqual(cm.exception.status_code, 400) + + +class TestGetUser(unittest.IsolatedAsyncioTestCase): + @patch("app.daos.users.retrieve_cache") + async def test_get_user_no_cache_no_user_found(self, mock_retrieve_cache): + # Mocking retrieve_cache to return no cached user + mock_retrieve_cache.return_value = None, None + + # Mocking the database session and query result + mock_session = AlchemyMagicMock() + mock_query = MagicMock() + mock_query.where.return_value = mock_query + mock_query.with_entities.return_value = mock_query + mock_query.first.return_value = None # Simulate no user found in the database + + mock_session.query.return_value = mock_query + + # Call the function and expect an exception + with self.assertRaises(HTTPException) as cm: + await get_user(0, mock_session) + # Assertions + mock_retrieve_cache.assert_called_once_with("user_0") + mock_query.where.assert_called_once() + self.assertEqual(cm.exception.status_code, 400) + self.assertEqual(str(cm.exception.detail), "No User found for given ID.") + + # Write similar tests for other scenarios (e.g., cache hit, database query exception, cache creation exception) + + +login_data = Login(email="test@gmail.com", password="Test@123") + + +# Test if user login is successful +@patch("app.constants.jwt_utils.create_access_token") +@patch("app.daos.users.check_password_hash") +def test_login_successful(mock_create_access_token, mock_check_password_hash): + mock_create_access_token.return_value = True + mock_check_password_hash.return_value = True + + mock_db_session = AlchemyMagicMock() + User = namedtuple("User", ["id", "email", "password"]) + user = User(id=1, email="test@gmail.com", password=generate_password_hash("Test@123", method="pbkdf2")) + mock_db_session.query.return_value.where.return_value.first.return_value = user + + response = login(login_data, mock_db_session) + expected_response = { + "success": True, + "message": "User logged in successfully.", + "data": {"token": True}, + } + assert response == expected_response + + +def test_login_invalid_password(): + mock_db_session = AlchemyMagicMock() + + User = namedtuple("User", ["id", "email", "password"]) + user = User(id=1, email="test@gmail.com", password="Test@13") + + mock_db_session.query.return_value.where.return_value.first.return_value = user + + with pytest.raises(Exception): + login(login_data, mock_db_session) + + +if __name__ == "__main__": + unittest.main() diff --git a/app/utils/user_utils.py b/app/utils/user_utils.py index 70fc6b3..94394fd 100644 --- a/app/utils/user_utils.py +++ b/app/utils/user_utils.py @@ -25,7 +25,7 @@ async def get_current_user(request: Request): raise HTTPException(status_code=401, detail="Token not provided") try: - user_id = int(dict(request).get("path_params")["user_id"]) # noqa + user_id = int(dict(request).get("path_params")["user_id"]) # type: ignore token = token.split(" ")[1] payload = jwt_utils.decode_access_token(token) if user_id == int(payload["id"]): diff --git a/requirements.txt b/requirements.txt index 0c37759..a8ce72e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ aiohttp>3.4 aiosignal==1.3.1 +alchemy-mock alembic>1 anyio==3.7.0 argilla>1.1 @@ -22,10 +23,11 @@ dataclasses-json==0.5.7 Deprecated==1.2.14 email_validator et-xmlfile==1.1.0 -fastapi-pagination fastapi==0.95.2 +fastapi-pagination flake8 flatbuffers==23.5.26 +freezegun==1.4.0 frozenlist==1.3.3 ftfy==6.1.1 greenlet==2.0.2 @@ -43,8 +45,8 @@ langchain lxml==4.9.2 lz4==4.3.2 Markdown==3.4.3 -marshmallow-enum==1.5.1 marshmallow==3.19.0 +marshmallow-enum==1.5.1 monotonic==1.6 mpmath==1.3.0 msg-parser==1.2.0 @@ -71,10 +73,10 @@ pyjwt pylint PyMySQL==1.0.3 pypandoc==1.11 +pytest==7.3.1 pytest-asyncio pytest-cov pytest-sqlalchemy-mock==0.1.5 -pytest==7.3.1 pytest_mock_resources python-dateutil==2.8.2 python-docx==0.8.11 @@ -113,4 +115,4 @@ wrapt==1.14.0 xlrd==2.0.1 XlsxWriter==3.1.2 yarl==1.9.2 -zstandard==0.21.0 \ No newline at end of file +zstandard==0.21.0