diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac912af7..1f8144b9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,6 +9,18 @@ jobs: runs-on: ubuntu-latest if: ${{ !contains(github.event.pull_request.title, '[skip tests]') }} + env: + MONGODB_URI: mongodb://db:27017 + DB_NAME: todo-app + GOOGLE_JWT_SECRET_KEY: "test-secret-key-for-jwt" + GOOGLE_JWT_ACCESS_LIFETIME: "3600" + GOOGLE_JWT_REFRESH_LIFETIME: "604800" + GOOGLE_OAUTH_CLIENT_ID: "test-client-id" + GOOGLE_OAUTH_CLIENT_SECRET: "test-client-secret" + GOOGLE_OAUTH_REDIRECT_URI: "http://localhost:3000/auth/callback" + COOKIE_SECURE: "False" + COOKIE_SAMESITE: "Lax" + steps: - name: Checkout code uses: actions/checkout@v3 diff --git a/todo/exceptions/exception_handler.py b/todo/exceptions/exception_handler.py index 5db3796e..85632f93 100644 --- a/todo/exceptions/exception_handler.py +++ b/todo/exceptions/exception_handler.py @@ -18,7 +18,7 @@ GoogleRefreshTokenExpiredError, GoogleAPIException, GoogleUserNotFoundException, - GoogleTokenMissingError + GoogleTokenMissingError, ) @@ -89,7 +89,7 @@ def handle_exception(exc, context): authenticated=False, ) return Response(data=final_response_data.model_dump(mode="json", exclude_none=True), status=status_code) - + elif isinstance(exc, GoogleTokenMissingError): status_code = status.HTTP_401_UNAUTHORIZED error_list.append( diff --git a/todo/middlewares/jwt_auth.py b/todo/middlewares/jwt_auth.py index 07b08457..8559d404 100644 --- a/todo/middlewares/jwt_auth.py +++ b/todo/middlewares/jwt_auth.py @@ -30,12 +30,16 @@ def __call__(self, request): error_response = ApiErrorResponse( statusCode=status.HTTP_401_UNAUTHORIZED, message=AuthErrorMessages.AUTHENTICATION_REQUIRED, - errors=[ApiErrorDetail( - title=ApiErrors.AUTHENTICATION_FAILED.format(""), - detail=AuthErrorMessages.AUTHENTICATION_REQUIRED - )], + errors=[ + ApiErrorDetail( + title=ApiErrors.AUTHENTICATION_FAILED, + detail=AuthErrorMessages.AUTHENTICATION_REQUIRED, + ) + ], + ) + return JsonResponse( + data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED ) - return JsonResponse(data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED) except (TokenMissingError, TokenExpiredError, TokenInvalidError) as e: return self._handle_rds_auth_error(e) @@ -44,13 +48,17 @@ def __call__(self, request): except Exception: error_response = ApiErrorResponse( statusCode=status.HTTP_401_UNAUTHORIZED, - message=ApiErrors.AUTHENTICATION_FAILED.format(""), - errors=[ApiErrorDetail( - title=ApiErrors.AUTHENTICATION_FAILED.format(""), - detail=AuthErrorMessages.AUTHENTICATION_REQUIRED - )], + message=ApiErrors.AUTHENTICATION_FAILED, + errors=[ + ApiErrorDetail( + title=ApiErrors.AUTHENTICATION_FAILED, + detail=AuthErrorMessages.AUTHENTICATION_REQUIRED, + ) + ], + ) + return JsonResponse( + data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED ) - return JsonResponse(data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED) def _try_authentication(self, request) -> bool: if self._try_google_auth(request): @@ -111,23 +119,21 @@ def _handle_rds_auth_error(self, exception): error_response = ApiErrorResponse( statusCode=status.HTTP_401_UNAUTHORIZED, message=str(exception), - errors=[ApiErrorDetail( - title=ApiErrors.AUTHENTICATION_FAILED.format(""), - detail=str(exception) - )], + errors=[ApiErrorDetail(title=ApiErrors.AUTHENTICATION_FAILED, detail=str(exception))], + ) + return JsonResponse( + data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED ) - return JsonResponse(data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED) def _handle_google_auth_error(self, exception): error_response = ApiErrorResponse( statusCode=status.HTTP_401_UNAUTHORIZED, message=str(exception), - errors=[ApiErrorDetail( - title=ApiErrors.AUTHENTICATION_FAILED.format(""), - detail=str(exception) - )], + errors=[ApiErrorDetail(title=ApiErrors.AUTHENTICATION_FAILED, detail=str(exception))], + ) + return JsonResponse( + data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED ) - return JsonResponse(data=error_response.model_dump(mode="json", exclude_none=True), status=status.HTTP_401_UNAUTHORIZED) def is_google_user(request) -> bool: diff --git a/todo/tests/fixtures/user.py b/todo/tests/fixtures/user.py new file mode 100644 index 00000000..2dfffbe4 --- /dev/null +++ b/todo/tests/fixtures/user.py @@ -0,0 +1,18 @@ +from datetime import datetime, timezone + +users_db_data = [ + { + "google_id": "123456789", + "email_id": "test@example.com", + "name": "Test User", + "created_at": datetime.now(timezone.utc), + "updated_at": datetime.now(timezone.utc), + }, + { + "google_id": "987654321", + "email_id": "another@example.com", + "name": "Another User", + "created_at": datetime.now(timezone.utc), + "updated_at": datetime.now(timezone.utc), + }, +] diff --git a/todo/tests/integration/test_task_detail_api.py b/todo/tests/integration/test_task_detail_api.py index 0cb37ef1..72ea5075 100644 --- a/todo/tests/integration/test_task_detail_api.py +++ b/todo/tests/integration/test_task_detail_api.py @@ -1,13 +1,31 @@ from http import HTTPStatus -from bson import ObjectId from django.urls import reverse -from rest_framework.test import APIClient +from bson import ObjectId + from todo.tests.fixtures.task import tasks_db_data from todo.tests.integration.base_mongo_test import BaseMongoTestCase from todo.constants.messages import ApiErrors, ValidationErrors +from todo.utils.google_jwt_utils import generate_google_token_pair + + +class AuthenticatedMongoTestCase(BaseMongoTestCase): + def setUp(self): + super().setUp() + self._setup_auth_cookies() + + def _setup_auth_cookies(self): + user_data = { + "user_id": str(ObjectId()), + "google_id": "test_google_id", + "email": "test@example.com", + "name": "Test User", + } + tokens = generate_google_token_pair(user_data) + self.client.cookies["ext-access"] = tokens["access_token"] + self.client.cookies["ext-refresh"] = tokens["refresh_token"] -class TaskDetailAPIIntegrationTest(BaseMongoTestCase): +class TaskDetailAPIIntegrationTest(AuthenticatedMongoTestCase): def setUp(self): super().setUp() self.db.tasks.delete_many({}) # Clear tasks to avoid DuplicateKeyError @@ -17,7 +35,6 @@ def setUp(self): self.existing_task_id = str(self.task_doc["_id"]) self.non_existent_id = str(ObjectId()) self.invalid_task_id = "invalid-task-id" - self.client = APIClient() def test_get_task_by_id_success(self): url = reverse("task_detail", args=[self.existing_task_id]) diff --git a/todo/tests/integration/test_tasks_delete.py b/todo/tests/integration/test_tasks_delete.py index 099d7ec5..915b9301 100644 --- a/todo/tests/integration/test_tasks_delete.py +++ b/todo/tests/integration/test_tasks_delete.py @@ -1,13 +1,31 @@ from http import HTTPStatus -from bson import ObjectId from django.urls import reverse -from rest_framework.test import APIClient +from bson import ObjectId + from todo.tests.fixtures.task import tasks_db_data from todo.tests.integration.base_mongo_test import BaseMongoTestCase from todo.constants.messages import ValidationErrors, ApiErrors +from todo.utils.google_jwt_utils import generate_google_token_pair + + +class AuthenticatedMongoTestCase(BaseMongoTestCase): + def setUp(self): + super().setUp() + self._setup_auth_cookies() + + def _setup_auth_cookies(self): + user_data = { + "user_id": str(ObjectId()), + "google_id": "test_google_id", + "email": "test@example.com", + "name": "Test User", + } + tokens = generate_google_token_pair(user_data) + self.client.cookies["ext-access"] = tokens["access_token"] + self.client.cookies["ext-refresh"] = tokens["refresh_token"] -class TaskDeleteAPIIntegrationTest(BaseMongoTestCase): +class TaskDeleteAPIIntegrationTest(AuthenticatedMongoTestCase): def setUp(self): super().setUp() self.db.tasks.delete_many({}) @@ -17,7 +35,6 @@ def setUp(self): self.existing_task_id = str(task_doc["_id"]) self.non_existent_id = str(ObjectId()) self.invalid_task_id = "invalid-task-id" - self.client = APIClient() def test_delete_task_success(self): url = reverse("task_detail", args=[self.existing_task_id]) diff --git a/todo/tests/unit/exceptions/test_exception_handler.py b/todo/tests/unit/exceptions/test_exception_handler.py index 11f5c7c6..b660b1ee 100644 --- a/todo/tests/unit/exceptions/test_exception_handler.py +++ b/todo/tests/unit/exceptions/test_exception_handler.py @@ -12,25 +12,25 @@ class ExceptionHandlerTests(TestCase): - @patch("todo.exceptions.exception_handler.format_validation_errors") - def test_returns_400_for_validation_error(self, mock_format_validation_errors: Mock): - validation_error = DRFValidationError(detail={"field": ["error message"]}) - mock_format_validation_errors.return_value = [ - ApiErrorDetail(detail="error message", source={ApiErrorSource.PARAMETER: "field"}) - ] - - response = handle_exception(validation_error, {}) - - self.assertIsInstance(response, Response) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - expected_response = { - "statusCode": 400, - "message": "Invalid request", - "errors": [{"source": {"parameter": "field"}, "detail": "error message"}], - } - self.assertDictEqual(response.data, expected_response) - - mock_format_validation_errors.assert_called_once_with(validation_error.detail) + def test_returns_400_for_validation_error(self): + error_detail = {"field": ["error message"]} + exception = DRFValidationError(detail=error_detail) + request = Mock() + + with patch("todo.exceptions.exception_handler.format_validation_errors") as mock_format: + mock_format.return_value = [ + ApiErrorDetail(detail="error message", source={ApiErrorSource.PARAMETER: "field"}) + ] + response = handle_exception(exception, {"request": request}) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + expected_response = { + "statusCode": 400, + "message": "error message", + "errors": [{"source": {"parameter": "field"}, "detail": "error message"}], + } + self.assertDictEqual(response.data, expected_response) + mock_format.assert_called_once_with(error_detail) def test_custom_handler_formats_generic_exception(self): request = None @@ -51,9 +51,9 @@ def test_custom_handler_formats_generic_exception(self): expected_detail_obj_in_list = ApiErrorDetail( detail=error_message if settings.DEBUG else ApiErrors.INTERNAL_SERVER_ERROR, - title=ApiErrors.UNEXPECTED_ERROR_OCCURRED, + title=error_message, ) - expected_main_message = ApiErrors.UNEXPECTED_ERROR_OCCURRED + expected_main_message = ApiErrors.INTERNAL_SERVER_ERROR self.assertEqual(response.data.get("statusCode"), status.HTTP_500_INTERNAL_SERVER_ERROR) self.assertEqual(response.data.get("message"), expected_main_message) diff --git a/todo/tests/unit/middlewares/__init__.py b/todo/tests/unit/middlewares/__init__.py new file mode 100644 index 00000000..9d445f48 --- /dev/null +++ b/todo/tests/unit/middlewares/__init__.py @@ -0,0 +1 @@ +# This file is required for Python to recognize this directory as a package diff --git a/todo/tests/unit/middlewares/test_jwt_auth.py b/todo/tests/unit/middlewares/test_jwt_auth.py new file mode 100644 index 00000000..2681898c --- /dev/null +++ b/todo/tests/unit/middlewares/test_jwt_auth.py @@ -0,0 +1,132 @@ +from unittest import TestCase +from unittest.mock import Mock, patch +from django.http import HttpRequest, JsonResponse +from django.conf import settings +from rest_framework import status +import json + +from todo.middlewares.jwt_auth import JWTAuthenticationMiddleware, is_google_user, is_rds_user, get_current_user_info +from todo.constants.messages import AuthErrorMessages + + +class JWTAuthenticationMiddlewareTests(TestCase): + def setUp(self): + self.get_response = Mock(return_value=JsonResponse({"data": "test"})) + self.middleware = JWTAuthenticationMiddleware(self.get_response) + self.request = Mock(spec=HttpRequest) + self.request.path = "/v1/tasks" + self.request.headers = {} + self.request.COOKIES = {} + self._original_public_paths = settings.PUBLIC_PATHS + settings.PUBLIC_PATHS = ["/v1/auth/google/login"] + self.addCleanup(setattr, settings, "PUBLIC_PATHS", self._original_public_paths) + + def test_public_path_authentication_bypass(self): + """Test that requests to public paths bypass authentication""" + self.request.path = "/v1/auth/google/login" + response = self.middleware(self.request) + self.get_response.assert_called_once_with(self.request) + self.assertEqual(response.status_code, 200) + + @patch("todo.middlewares.jwt_auth.JWTAuthenticationMiddleware._try_google_auth") + def test_google_auth_success(self, mock_google_auth): + """Test successful Google authentication""" + mock_google_auth.return_value = True + self.request.COOKIES = {"ext-access": "google_token"} + response = self.middleware(self.request) + mock_google_auth.assert_called_once_with(self.request) + self.get_response.assert_called_once_with(self.request) + self.assertEqual(response.status_code, 200) + + @patch("todo.middlewares.jwt_auth.JWTAuthenticationMiddleware._try_rds_auth") + def test_rds_auth_success(self, mock_rds_auth): + """Test successful RDS authentication""" + mock_rds_auth.return_value = True + self.request.COOKIES = {"rds_session_v2": "valid_token"} + response = self.middleware(self.request) + mock_rds_auth.assert_called_once_with(self.request) + self.get_response.assert_called_once_with(self.request) + self.assertEqual(response.status_code, 200) + + @patch("todo.middlewares.jwt_auth.JWTAuthenticationMiddleware._try_google_auth") + def test_google_token_expired(self, mock_google_auth): + """Test handling of expired Google token""" + mock_google_auth.return_value = False + self.request.COOKIES = {"ext-access": "expired_token"} + response = self.middleware(self.request) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + response_data = json.loads(response.content) + self.assertEqual(response_data["message"], AuthErrorMessages.AUTHENTICATION_REQUIRED) + + @patch("todo.middlewares.jwt_auth.JWTAuthenticationMiddleware._try_rds_auth") + def test_rds_token_invalid(self, mock_rds_auth): + """Test handling of invalid RDS token""" + mock_rds_auth.return_value = False + self.request.COOKIES = {"rds_session_v2": "invalid_token"} + response = self.middleware(self.request) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + response_data = json.loads(response.content) + self.assertEqual(response_data["message"], AuthErrorMessages.AUTHENTICATION_REQUIRED) + + def test_no_tokens_provided(self): + """Test handling of request with no tokens""" + response = self.middleware(self.request) + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + response_data = json.loads(response.content) + self.assertEqual(response_data["message"], AuthErrorMessages.AUTHENTICATION_REQUIRED) + + +class AuthUtilityFunctionsTests(TestCase): + def setUp(self): + self.request = Mock(spec=HttpRequest) + + def test_is_google_user(self): + """Test checking if request is from Google user""" + self.request.auth_type = "google" + self.assertTrue(is_google_user(self.request)) + + self.request.auth_type = None + self.assertFalse(is_google_user(self.request)) + + self.request.auth_type = "rds" + self.assertFalse(is_google_user(self.request)) + + def test_is_rds_user(self): + """Test checking if request is from RDS user""" + self.request.auth_type = "rds" + self.assertTrue(is_rds_user(self.request)) + + self.request.auth_type = None + self.assertFalse(is_rds_user(self.request)) + + self.request.auth_type = "google" + self.assertFalse(is_rds_user(self.request)) + + def test_get_current_user_info_google(self): + """Test getting user info for Google user""" + self.request.user_id = "google_user_123" + self.request.auth_type = "google" + self.request.google_id = "google_123" + self.request.user_email = "test@example.com" + self.request.user_name = "Test User" + user_info = get_current_user_info(self.request) + self.assertEqual(user_info["user_id"], "google_user_123") + self.assertEqual(user_info["auth_type"], "google") + self.assertEqual(user_info["google_id"], "google_123") + self.assertEqual(user_info["email"], "test@example.com") + self.assertEqual(user_info["name"], "Test User") + + def test_get_current_user_info_rds(self): + """Test getting user info for RDS user""" + self.request.user_id = "rds_user_123" + self.request.auth_type = "rds" + self.request.user_role = "admin" + user_info = get_current_user_info(self.request) + self.assertEqual(user_info["user_id"], "rds_user_123") + self.assertEqual(user_info["auth_type"], "rds") + self.assertEqual(user_info["role"], "admin") + + def test_get_current_user_info_no_user_id(self): + """Test getting user info when no user ID is present""" + user_info = get_current_user_info(self.request) + self.assertIsNone(user_info) diff --git a/todo/tests/unit/models/test_user.py b/todo/tests/unit/models/test_user.py new file mode 100644 index 00000000..23e58ddc --- /dev/null +++ b/todo/tests/unit/models/test_user.py @@ -0,0 +1,55 @@ +from unittest import TestCase +from datetime import datetime, timezone +from pydantic import ValidationError +from todo.models.user import UserModel +from todo.tests.fixtures.user import users_db_data + + +class UserModelTest(TestCase): + def setUp(self) -> None: + self.valid_user_data = users_db_data[0] + + def test_user_model_instantiates_with_valid_data(self): + user = UserModel(**self.valid_user_data) + + self.assertEqual(user.google_id, self.valid_user_data["google_id"]) + self.assertEqual(user.email_id, self.valid_user_data["email_id"]) + self.assertEqual(user.name, self.valid_user_data["name"]) + self.assertEqual(user.created_at, self.valid_user_data["created_at"]) + self.assertEqual(user.updated_at, self.valid_user_data["updated_at"]) + + def test_user_model_throws_error_when_missing_required_fields(self): + required_fields = ["google_id", "email_id", "name"] + + for field in required_fields: + with self.subTest(f"missing field: {field}"): + incomplete_data = self.valid_user_data.copy() + incomplete_data.pop(field, None) + + with self.assertRaises(ValidationError) as context: + UserModel(**incomplete_data) + + error_fields = [e["loc"][0] for e in context.exception.errors()] + self.assertIn(field, error_fields) + + def test_user_model_throws_error_when_invalid_email(self): + invalid_data = self.valid_user_data.copy() + invalid_data["email_id"] = "invalid-email" + + with self.assertRaises(ValidationError) as context: + UserModel(**invalid_data) + + error_fields = [e["loc"][0] for e in context.exception.errors()] + self.assertIn("email_id", error_fields) + + def test_user_model_sets_default_timestamps(self): + minimal_data = { + "google_id": self.valid_user_data["google_id"], + "email_id": self.valid_user_data["email_id"], + "name": self.valid_user_data["name"], + } + user = UserModel(**minimal_data) + + self.assertIsInstance(user.created_at, datetime) + self.assertIsNone(user.updated_at) + self.assertLessEqual(user.created_at, datetime.now(timezone.utc)) diff --git a/todo/tests/unit/repositories/test_user_repository.py b/todo/tests/unit/repositories/test_user_repository.py new file mode 100644 index 00000000..3a93c7ba --- /dev/null +++ b/todo/tests/unit/repositories/test_user_repository.py @@ -0,0 +1,94 @@ +from unittest import TestCase +from unittest.mock import patch, MagicMock +from bson import ObjectId + +from todo.repositories.user_repository import UserRepository +from todo.models.user import UserModel +from todo.models.common.pyobjectid import PyObjectId +from todo.exceptions.google_auth_exceptions import GoogleUserNotFoundException, GoogleAPIException +from todo.tests.fixtures.user import users_db_data +from todo.constants.messages import RepositoryErrors + + +class UserRepositoryTests(TestCase): + def setUp(self) -> None: + self.valid_user_data = {"google_id": "123456789", "email": "test@example.com", "name": "Test User"} + self.user_model = UserModel(**users_db_data[0]) + self.mock_collection = MagicMock() + self.mock_db_manager = MagicMock() + self.mock_db_manager.get_collection.return_value = self.mock_collection + + @patch("todo.repositories.user_repository.DatabaseManager") + def test_get_by_id_success(self, mock_db_manager): + mock_db_manager.return_value = self.mock_db_manager + user_id = str(ObjectId()) + self.mock_collection.find_one.return_value = users_db_data[0] + + result = UserRepository.get_by_id(user_id) + + self.mock_collection.find_one.assert_called_once_with({"_id": PyObjectId(user_id)}) + self.assertIsInstance(result, UserModel) + self.assertEqual(result.google_id, users_db_data[0]["google_id"]) + + @patch("todo.repositories.user_repository.DatabaseManager") + def test_get_by_id_not_found(self, mock_db_manager): + mock_db_manager.return_value = self.mock_db_manager + user_id = str(ObjectId()) + self.mock_collection.find_one.return_value = None + + result = UserRepository.get_by_id(user_id) + self.assertIsNone(result) + + @patch("todo.repositories.user_repository.DatabaseManager") + def test_get_by_id_database_error(self, mock_db_manager): + mock_db_manager.return_value = self.mock_db_manager + user_id = str(ObjectId()) + self.mock_collection.find_one.side_effect = Exception("Database error") + + with self.assertRaises(GoogleUserNotFoundException): + UserRepository.get_by_id(user_id) + + @patch("todo.repositories.user_repository.DatabaseManager") + def test_create_or_update_success(self, mock_db_manager): + mock_db_manager.return_value = self.mock_db_manager + self.mock_collection.find_one_and_update.return_value = users_db_data[0] + + result = UserRepository.create_or_update(self.valid_user_data) + + self.mock_collection.find_one_and_update.assert_called_once() + call_args = self.mock_collection.find_one_and_update.call_args[0] + self.assertEqual(call_args[0], {"google_id": self.valid_user_data["google_id"]}) + self.assertIsInstance(result, UserModel) + self.assertEqual(result.google_id, users_db_data[0]["google_id"]) + + @patch("todo.repositories.user_repository.DatabaseManager") + def test_create_or_update_no_result(self, mock_db_manager): + mock_db_manager.return_value = self.mock_db_manager + self.mock_collection.find_one_and_update.return_value = None + + with self.assertRaises(GoogleAPIException) as context: + UserRepository.create_or_update(self.valid_user_data) + self.assertIn(RepositoryErrors.USER_OPERATION_FAILED, str(context.exception)) + + @patch("todo.repositories.user_repository.DatabaseManager") + def test_create_or_update_database_error(self, mock_db_manager): + mock_db_manager.return_value = self.mock_db_manager + self.mock_collection.find_one_and_update.side_effect = Exception("Database error") + + with self.assertRaises(GoogleAPIException) as context: + UserRepository.create_or_update(self.valid_user_data) + self.assertIn(RepositoryErrors.USER_CREATE_UPDATE_FAILED.format("Database error"), str(context.exception)) + + @patch("todo.repositories.user_repository.DatabaseManager") + def test_create_or_update_sets_timestamps(self, mock_db_manager): + mock_db_manager.return_value = self.mock_db_manager + self.mock_collection.find_one_and_update.return_value = users_db_data[0] + + UserRepository.create_or_update(self.valid_user_data) + + call_args = self.mock_collection.find_one_and_update.call_args[0] + update_doc = call_args[1] + self.assertIn("$set", update_doc) + self.assertIn("updated_at", update_doc["$set"]) + self.assertIn("$setOnInsert", update_doc) + self.assertIn("created_at", update_doc["$setOnInsert"]) diff --git a/todo/tests/unit/services/test_google_oauth_service.py b/todo/tests/unit/services/test_google_oauth_service.py new file mode 100644 index 00000000..b312d2ee --- /dev/null +++ b/todo/tests/unit/services/test_google_oauth_service.py @@ -0,0 +1,141 @@ +from unittest import TestCase +from unittest.mock import patch, MagicMock +from urllib.parse import urlencode + +from todo.services.google_oauth_service import GoogleOAuthService +from todo.exceptions.google_auth_exceptions import GoogleAPIException, GoogleAuthException +from todo.constants.messages import ApiErrors + + +class GoogleOAuthServiceTests(TestCase): + def setUp(self) -> None: + self.mock_settings = { + "GOOGLE_OAUTH": { + "CLIENT_ID": "test-client-id", + "CLIENT_SECRET": "test-client-secret", + "REDIRECT_URI": "http://localhost:3000/auth/callback", + "SCOPES": ["email", "profile"], + } + } + self.valid_user_info = {"id": "123456789", "email": "test@example.com", "name": "Test User"} + self.valid_tokens = {"access_token": "test-access-token", "refresh_token": "test-refresh-token"} + + @patch("todo.services.google_oauth_service.settings") + @patch("todo.services.google_oauth_service.secrets") + def test_get_authorization_url_success(self, mock_secrets, mock_settings): + mock_settings.configure_mock(**self.mock_settings) + mock_secrets.token_urlsafe.return_value = "test-state" + + auth_url, state = GoogleOAuthService.get_authorization_url() + + self.assertEqual(state, "test-state") + expected_params = { + "client_id": self.mock_settings["GOOGLE_OAUTH"]["CLIENT_ID"], + "redirect_uri": self.mock_settings["GOOGLE_OAUTH"]["REDIRECT_URI"], + "response_type": "code", + "scope": " ".join(self.mock_settings["GOOGLE_OAUTH"]["SCOPES"]), + "access_type": "offline", + "prompt": "consent", + "state": state, + } + expected_url = f"{GoogleOAuthService.GOOGLE_AUTH_URL}?{urlencode(expected_params)}" + self.assertEqual(auth_url, expected_url) + + @patch("todo.services.google_oauth_service.settings") + def test_get_authorization_url_error(self, mock_settings): + mock_settings.configure_mock(**self.mock_settings) + mock_settings.GOOGLE_OAUTH = None + + with self.assertRaises(GoogleAuthException) as context: + GoogleOAuthService.get_authorization_url() + self.assertIn(ApiErrors.GOOGLE_AUTH_FAILED, str(context.exception)) + + @patch("todo.services.google_oauth_service.GoogleOAuthService._exchange_code_for_tokens") + @patch("todo.services.google_oauth_service.GoogleOAuthService._get_user_info") + def test_handle_callback_success(self, mock_get_user_info, mock_exchange_tokens): + mock_exchange_tokens.return_value = self.valid_tokens + mock_get_user_info.return_value = self.valid_user_info + + result = GoogleOAuthService.handle_callback("test-code") + + self.assertEqual(result["google_id"], self.valid_user_info["id"]) + self.assertEqual(result["email"], self.valid_user_info["email"]) + self.assertEqual(result["name"], self.valid_user_info["name"]) + mock_exchange_tokens.assert_called_once_with("test-code") + mock_get_user_info.assert_called_once_with(self.valid_tokens["access_token"]) + + @patch("todo.services.google_oauth_service.GoogleOAuthService._exchange_code_for_tokens") + def test_handle_callback_token_error(self, mock_exchange_tokens): + mock_exchange_tokens.side_effect = GoogleAPIException(ApiErrors.TOKEN_EXCHANGE_FAILED) + + with self.assertRaises(GoogleAPIException) as context: + GoogleOAuthService.handle_callback("test-code") + self.assertIn(ApiErrors.TOKEN_EXCHANGE_FAILED, str(context.exception)) + + @patch("todo.services.google_oauth_service.requests.post") + @patch("todo.services.google_oauth_service.settings") + def test_exchange_code_for_tokens_success(self, mock_settings, mock_post): + mock_settings.configure_mock(**self.mock_settings) + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = self.valid_tokens + mock_post.return_value = mock_response + + result = GoogleOAuthService._exchange_code_for_tokens("test-code") + + self.assertEqual(result, self.valid_tokens) + mock_post.assert_called_once() + call_args = mock_post.call_args[1] + self.assertEqual(call_args["data"]["code"], "test-code") + self.assertEqual(call_args["data"]["client_id"], "test-client-id") + self.assertEqual(call_args["data"]["client_secret"], "test-client-secret") + + @patch("todo.services.google_oauth_service.requests.post") + @patch("todo.services.google_oauth_service.settings") + def test_exchange_code_for_tokens_error_response(self, mock_settings, mock_post): + mock_settings.configure_mock(**self.mock_settings) + mock_response = MagicMock() + mock_response.status_code = 400 + mock_post.return_value = mock_response + + with self.assertRaises(GoogleAPIException) as context: + GoogleOAuthService._exchange_code_for_tokens("test-code") + self.assertIn(ApiErrors.TOKEN_EXCHANGE_FAILED, str(context.exception)) + + @patch("todo.services.google_oauth_service.requests.get") + def test_get_user_info_success(self, mock_get): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = self.valid_user_info + mock_get.return_value = mock_response + + result = GoogleOAuthService._get_user_info("test-token") + + self.assertEqual(result, self.valid_user_info) + mock_get.assert_called_once() + call_args = mock_get.call_args[1] + self.assertEqual(call_args["headers"]["Authorization"], "Bearer test-token") + + @patch("todo.services.google_oauth_service.requests.get") + def test_get_user_info_missing_fields(self, mock_get): + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"id": "123"} + mock_get.return_value = mock_response + + with self.assertRaises(GoogleAPIException) as context: + GoogleOAuthService._get_user_info("test-token") + error_msg = str(context.exception) + self.assertIn(ApiErrors.MISSING_USER_INFO_FIELDS.split(":")[0], error_msg) + for field in ("email", "name"): + self.assertIn(field, error_msg) + + @patch("todo.services.google_oauth_service.requests.get") + def test_get_user_info_error_response(self, mock_get): + mock_response = MagicMock() + mock_response.status_code = 400 + mock_get.return_value = mock_response + + with self.assertRaises(GoogleAPIException) as context: + GoogleOAuthService._get_user_info("test-token") + self.assertIn(ApiErrors.USER_INFO_FETCH_FAILED.format("HTTP error"), str(context.exception)) diff --git a/todo/tests/unit/services/test_user_service.py b/todo/tests/unit/services/test_user_service.py new file mode 100644 index 00000000..14183775 --- /dev/null +++ b/todo/tests/unit/services/test_user_service.py @@ -0,0 +1,87 @@ +from unittest import TestCase +from unittest.mock import patch +from rest_framework.exceptions import ValidationError as DRFValidationError + +from todo.services.user_service import UserService +from todo.models.user import UserModel +from todo.exceptions.google_auth_exceptions import GoogleUserNotFoundException, GoogleAPIException +from todo.tests.fixtures.user import users_db_data +from todo.constants.messages import ValidationErrors, RepositoryErrors + + +class UserServiceTests(TestCase): + def setUp(self) -> None: + self.valid_google_user_data = {"google_id": "123456789", "email": "test@example.com", "name": "Test User"} + self.user_model = UserModel(**users_db_data[0]) + + @patch("todo.services.user_service.UserRepository") + def test_create_or_update_user_success(self, mock_repository): + mock_repository.create_or_update.return_value = self.user_model + + result = UserService.create_or_update_user(self.valid_google_user_data) + + mock_repository.create_or_update.assert_called_once_with(self.valid_google_user_data) + self.assertEqual(result, self.user_model) + + @patch("todo.services.user_service.UserRepository") + def test_create_or_update_user_validation_error(self, mock_repository): + invalid_data = {"google_id": "123"} + + with self.assertRaises(DRFValidationError) as context: + UserService.create_or_update_user(invalid_data) + self.assertIn(ValidationErrors.MISSING_EMAIL, str(context.exception.detail)) + self.assertIn(ValidationErrors.MISSING_NAME, str(context.exception.detail)) + mock_repository.create_or_update.assert_not_called() + + @patch("todo.services.user_service.UserRepository") + def test_create_or_update_user_repository_error(self, mock_repository): + mock_repository.create_or_update.side_effect = Exception("Database error") + + with self.assertRaises(GoogleAPIException) as context: + UserService.create_or_update_user(self.valid_google_user_data) + self.assertIn(RepositoryErrors.USER_CREATE_UPDATE_FAILED.format("Database error"), str(context.exception)) + + @patch("todo.services.user_service.UserRepository") + def test_get_user_by_id_success(self, mock_repository): + mock_repository.get_by_id.return_value = self.user_model + + result = UserService.get_user_by_id("123") + + mock_repository.get_by_id.assert_called_once_with("123") + self.assertEqual(result, self.user_model) + + @patch("todo.services.user_service.UserRepository") + def test_get_user_by_id_not_found(self, mock_repository): + mock_repository.get_by_id.return_value = None + + with self.assertRaises(GoogleUserNotFoundException): + UserService.get_user_by_id("123") + mock_repository.get_by_id.assert_called_once_with("123") + + def test_validate_google_user_data_success(self): + try: + UserService._validate_google_user_data(self.valid_google_user_data) + except DRFValidationError: + self.fail("ValidationError raised unexpectedly!") + + def test_validate_google_user_data_missing_fields(self): + test_cases = [ + {"email": "test@example.com", "name": "Test User"}, + {"google_id": "123", "name": "Test User"}, + {"google_id": "123", "email": "test@example.com"}, + ] + + for invalid_data in test_cases: + with self.subTest(f"Testing missing field in {invalid_data}"): + with self.assertRaises(DRFValidationError) as context: + UserService._validate_google_user_data(invalid_data) + + error_dict = context.exception.detail + self.assertTrue(len(error_dict) > 0) + + if "google_id" not in invalid_data: + self.assertIn(ValidationErrors.MISSING_GOOGLE_ID, str(error_dict)) + if "email" not in invalid_data: + self.assertIn(ValidationErrors.MISSING_EMAIL, str(error_dict)) + if "name" not in invalid_data: + self.assertIn(ValidationErrors.MISSING_NAME, str(error_dict)) diff --git a/todo/tests/unit/views/test_auth.py b/todo/tests/unit/views/test_auth.py new file mode 100644 index 00000000..b1545896 --- /dev/null +++ b/todo/tests/unit/views/test_auth.py @@ -0,0 +1,270 @@ +from rest_framework.test import APISimpleTestCase, APIClient, APIRequestFactory +from rest_framework.reverse import reverse +from rest_framework import status +from unittest.mock import patch, Mock, PropertyMock +from bson.objectid import ObjectId + +from todo.views.auth import ( + GoogleCallbackView, +) + +from todo.utils.google_jwt_utils import ( + generate_google_token_pair, +) +from todo.constants.messages import AppMessages, AuthErrorMessages + + +class GoogleLoginViewTests(APISimpleTestCase): + def setUp(self): + super().setUp() + self.client = APIClient() + self.url = reverse("google_login") + + @patch("todo.services.google_oauth_service.GoogleOAuthService.get_authorization_url") + def test_get_returns_redirect_url_for_html_request(self, mock_get_auth_url): + mock_auth_url = "https://accounts.google.com/o/oauth2/auth" + mock_state = "test_state" + mock_get_auth_url.return_value = (mock_auth_url, mock_state) + + response = self.client.get(self.url) + + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertEqual(response.url, mock_auth_url) + mock_get_auth_url.assert_called_once_with(None) + + @patch("todo.services.google_oauth_service.GoogleOAuthService.get_authorization_url") + def test_get_returns_json_for_json_request(self, mock_get_auth_url): + mock_auth_url = "https://accounts.google.com/o/oauth2/auth" + mock_state = "test_state" + mock_get_auth_url.return_value = (mock_auth_url, mock_state) + + response = self.client.get(self.url, HTTP_ACCEPT="application/json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["data"]["authUrl"], mock_auth_url) + self.assertEqual(response.data["data"]["state"], mock_state) + mock_get_auth_url.assert_called_once_with(None) + + @patch("todo.services.google_oauth_service.GoogleOAuthService.get_authorization_url") + def test_get_with_redirect_url(self, mock_get_auth_url): + mock_auth_url = "https://accounts.google.com/o/oauth2/auth" + mock_state = "test_state" + mock_get_auth_url.return_value = (mock_auth_url, mock_state) + redirect_url = "http://localhost:3000/callback" + + response = self.client.get(f"{self.url}?redirectURL={redirect_url}") + + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertEqual(response.url, mock_auth_url) + mock_get_auth_url.assert_called_once_with(redirect_url) + + +class GoogleCallbackViewTests(APISimpleTestCase): + def setUp(self): + super().setUp() + self.client = APIClient() + self.url = reverse("google_callback") + self.factory = APIRequestFactory() + self.view = GoogleCallbackView.as_view() + + def test_get_returns_error_for_oauth_error(self): + error = "access_denied" + request = self.factory.get(f"{self.url}?error={error}") + + response = self.view(request) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["message"], error) + self.assertEqual(response.data["errors"][0]["detail"], error) + + def test_get_returns_error_for_missing_code(self): + request = self.factory.get(self.url) + + response = self.view(request) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["message"], "No authorization code received from Google") + self.assertEqual(response.data["errors"][0]["detail"], "No authorization code received from Google") + + def test_get_returns_error_for_invalid_state(self): + request = self.factory.get(f"{self.url}?code=test_code&state=invalid_state") + request.session = {"oauth_state": "different_state"} + + response = self.view(request) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.data["message"], "Invalid state parameter") + self.assertEqual(response.data["errors"][0]["detail"], "Invalid state parameter") + + @patch("todo.services.google_oauth_service.GoogleOAuthService.handle_callback") + @patch("todo.services.user_service.UserService.create_or_update_user") + def test_get_handles_callback_successfully(self, mock_create_user, mock_handle_callback): + mock_google_data = { + "id": "test_google_id", + "email": "test@example.com", + "name": "Test User", + } + user_id = str(ObjectId()) + mock_user = Mock() + mock_user.id = ObjectId(user_id) + mock_user.google_id = mock_google_data["id"] + mock_user.email_id = mock_google_data["email"] + mock_user.name = mock_google_data["name"] + type(mock_user).id = PropertyMock(return_value=ObjectId(user_id)) + + mock_handle_callback.return_value = mock_google_data + mock_create_user.return_value = mock_user + + request = self.factory.get(f"{self.url}?code=test_code&state=test_state") + request.session = {"oauth_state": "test_state"} + + response = self.view(request) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIn("✅ Google OAuth Login Successful!", response.content.decode()) + self.assertIn(str(mock_user.id), response.content.decode()) + self.assertIn(mock_user.name, response.content.decode()) + self.assertIn(mock_user.email_id, response.content.decode()) + self.assertIn(mock_user.google_id, response.content.decode()) + self.assertIn("ext-access", response.cookies) + self.assertIn("ext-refresh", response.cookies) + self.assertNotIn("oauth_state", request.session) + + +class GoogleAuthStatusViewTests(APISimpleTestCase): + def setUp(self): + super().setUp() + self.client = APIClient() + self.url = reverse("google_status") + + def test_get_returns_401_when_no_access_token(self): + response = self.client.get(self.url) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(response.data["message"], AuthErrorMessages.NO_ACCESS_TOKEN) + self.assertEqual(response.data["authenticated"], False) + self.assertEqual(response.data["statusCode"], status.HTTP_401_UNAUTHORIZED) + + @patch("todo.utils.google_jwt_utils.validate_google_access_token") + @patch("todo.services.user_service.UserService.get_user_by_id") + def test_get_returns_user_info_when_authenticated(self, mock_get_user, mock_validate_token): + user_id = str(ObjectId()) + user_data = { + "user_id": user_id, + "google_id": "test_google_id", + "email": "test@example.com", + "name": "Test User", + } + mock_validate_token.return_value = user_data + + mock_user = Mock() + mock_user.id = ObjectId(user_id) + mock_user.google_id = "test_google_id" + mock_user.email_id = "test@example.com" + mock_user.name = "Test User" + type(mock_user).id = PropertyMock(return_value=ObjectId(user_id)) + + mock_get_user.return_value = mock_user + + tokens = generate_google_token_pair(user_data) + self.client.cookies["ext-access"] = tokens["access_token"] + + response = self.client.get(self.url, HTTP_ACCEPT="application/json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["data"]["user"]["id"], user_id) + self.assertEqual(response.data["data"]["user"]["email"], mock_user.email_id) + self.assertEqual(response.data["data"]["user"]["name"], mock_user.name) + self.assertEqual(response.data["data"]["user"]["google_id"], mock_user.google_id) + + +class GoogleRefreshViewTests(APISimpleTestCase): + def setUp(self): + super().setUp() + self.client = APIClient() + self.url = reverse("google_refresh") + + def test_get_returns_401_when_no_refresh_token(self): + response = self.client.get(self.url) + + self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) + self.assertEqual(response.data["message"], AuthErrorMessages.NO_REFRESH_TOKEN) + self.assertEqual(response.data["authenticated"], False) + self.assertEqual(response.data["statusCode"], status.HTTP_401_UNAUTHORIZED) + + @patch("todo.utils.google_jwt_utils.validate_google_refresh_token") + def test_get_refreshes_token_successfully(self, mock_validate_token): + user_data = { + "user_id": str(ObjectId()), + "google_id": "test_google_id", + "email": "test@example.com", + "name": "Test User", + } + tokens = generate_google_token_pair(user_data) + mock_validate_token.return_value = user_data + + self.client.cookies["ext-refresh"] = tokens["refresh_token"] + + response = self.client.get(self.url, HTTP_ACCEPT="application/json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["data"]["success"], True) + self.assertEqual(response.data["message"], AppMessages.TOKEN_REFRESHED) + self.assertIn("ext-access", response.cookies) + + +class GoogleLogoutViewTests(APISimpleTestCase): + def setUp(self): + super().setUp() + self.client = APIClient() + self.url = reverse("google_logout") + + def test_get_returns_success_and_clears_cookies(self): + user_data = { + "user_id": str(ObjectId()), + "google_id": "test_google_id", + "email": "test@example.com", + "name": "Test User", + } + tokens = generate_google_token_pair(user_data) + self.client.cookies["ext-access"] = tokens["access_token"] + self.client.cookies["ext-refresh"] = tokens["refresh_token"] + + response = self.client.get(self.url, HTTP_ACCEPT="application/json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["data"]["success"], True) + self.assertEqual(response.data["message"], AppMessages.GOOGLE_LOGOUT_SUCCESS) + self.assertEqual(response.cookies.get("ext-access").value, "") + self.assertEqual(response.cookies.get("ext-refresh").value, "") + + def test_get_redirects_when_not_json_request(self): + redirect_url = "http://localhost:3000" + self.client.cookies["ext-access"] = "test_access_token" + self.client.cookies["ext-refresh"] = "test_refresh_token" + + response = self.client.get(f"{self.url}?redirectURL={redirect_url}") + + self.assertEqual(response.status_code, status.HTTP_302_FOUND) + self.assertEqual(response.url, redirect_url) + self.assertEqual(response.cookies.get("ext-access").value, "") + self.assertEqual(response.cookies.get("ext-refresh").value, "") + + def test_post_returns_success_and_clears_cookies(self): + user_data = { + "user_id": str(ObjectId()), + "google_id": "test_google_id", + "email": "test@example.com", + "name": "Test User", + } + tokens = generate_google_token_pair(user_data) + self.client.cookies["ext-access"] = tokens["access_token"] + self.client.cookies["ext-refresh"] = tokens["refresh_token"] + + response = self.client.post(self.url, HTTP_ACCEPT="application/json") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["data"]["success"], True) + self.assertEqual(response.data["message"], AppMessages.GOOGLE_LOGOUT_SUCCESS) + self.assertEqual(response.cookies.get("ext-access").value, "") + self.assertEqual(response.cookies.get("ext-refresh").value, "") diff --git a/todo/tests/unit/views/test_task.py b/todo/tests/unit/views/test_task.py index a05f0c05..48d066fc 100644 --- a/todo/tests/unit/views/test_task.py +++ b/todo/tests/unit/views/test_task.py @@ -20,11 +20,31 @@ from todo.constants.messages import ValidationErrors, ApiErrors from todo.dto.responses.error_response import ApiErrorResponse, ApiErrorDetail from rest_framework.exceptions import ValidationError as DRFValidationError +from todo.utils.google_jwt_utils import generate_google_token_pair -class TaskViewTests(APISimpleTestCase): +class AuthenticatedTestCase(APISimpleTestCase): def setUp(self): + super().setUp() self.client = APIClient() + self._setup_auth_cookies() + + def _setup_auth_cookies(self): + user_data = { + "user_id": str(ObjectId()), + "google_id": "test_google_id", + "email": "test@example.com", + "name": "Test User", + } + tokens = generate_google_token_pair(user_data) + + self.client.cookies["ext-access"] = tokens["access_token"] + self.client.cookies["ext-refresh"] = tokens["refresh_token"] + + +class TaskViewTests(AuthenticatedTestCase): + def setUp(self): + super().setUp() self.url = reverse("tasks") self.valid_params = {"page": 1, "limit": 10} @@ -59,7 +79,7 @@ def test_get_tasks_returns_400_for_invalid_query_params(self): self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) expected_response = { "statusCode": 400, - "message": "Invalid request", + "message": "A valid integer is required.", "errors": [ {"source": {"parameter": "page"}, "detail": "A valid integer is required."}, {"source": {"parameter": "limit"}, "detail": "limit must be greater than or equal to 1"}, @@ -130,7 +150,7 @@ def test_get_single_task_unexpected_error(self, mock_get_task_by_id: Mock): self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) self.assertEqual(response.data["statusCode"], status.HTTP_500_INTERNAL_SERVER_ERROR) - self.assertEqual(response.data["message"], ApiErrors.UNEXPECTED_ERROR_OCCURRED) + self.assertEqual(response.data["message"], ApiErrors.INTERNAL_SERVER_ERROR) self.assertEqual(response.data["errors"][0]["detail"], ApiErrors.INTERNAL_SERVER_ERROR) mock_get_task_by_id.assert_called_once_with(task_id) @@ -193,9 +213,9 @@ def test_get_tasks_with_non_numeric_parameters(self): self.assertTrue("page" in error_detail or "limit" in error_detail) -class CreateTaskViewTests(APISimpleTestCase): +class CreateTaskViewTests(AuthenticatedTestCase): def setUp(self): - self.client = APIClient() + super().setUp() self.url = reverse("tasks") self.valid_payload = { @@ -299,13 +319,14 @@ def test_create_task_returns_500_on_internal_error(self, mock_create_task): try: response = self.client.post(self.url, data=self.valid_payload, format="json") self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) - self.assertIn("An unexpected error occurred", str(response.data)) + self.assertEqual(response.data["message"], ApiErrors.INTERNAL_SERVER_ERROR) except Exception as e: self.assertEqual(str(e), "Database exploded") -class TaskDeleteViewTests(APISimpleTestCase): +class TaskDeleteViewTests(AuthenticatedTestCase): def setUp(self): + super().setUp() self.valid_task_id = str(ObjectId()) self.url = reverse("task_detail", kwargs={"task_id": self.valid_task_id}) @@ -333,9 +354,9 @@ def test_delete_task_returns_400_for_invalid_id_format(self, mock_delete_task: M self.assertIn(ValidationErrors.INVALID_TASK_ID_FORMAT, response.data["message"]) -class TaskDetailViewPatchTests(APISimpleTestCase): +class TaskDetailViewPatchTests(AuthenticatedTestCase): def setUp(self): - self.client = APIClient() + super().setUp() self.task_id_str = str(ObjectId()) self.task_url = reverse("task_detail", args=[self.task_id_str]) self.future_date = (datetime.now(timezone.utc) + timedelta(days=7)).isoformat() @@ -484,7 +505,7 @@ def test_patch_task_service_raises_drf_validation_error( self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(response.data["statusCode"], status.HTTP_400_BAD_REQUEST) - self.assertEqual(response.data["message"], "Invalid request") + self.assertEqual(response.data["message"], service_error_detail["labels"][0]) self.assertIn( "labels", @@ -539,7 +560,7 @@ def test_patch_task_service_raises_unhandled_exception( self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) self.assertEqual(response.data["statusCode"], status.HTTP_500_INTERNAL_SERVER_ERROR) - self.assertEqual(response.data["message"], ApiErrors.UNEXPECTED_ERROR_OCCURRED) + self.assertEqual(response.data["message"], ApiErrors.INTERNAL_SERVER_ERROR) self.assertEqual(response.data["errors"][0]["detail"], ApiErrors.INTERNAL_SERVER_ERROR) with patch.object(settings, "DEBUG", True): diff --git a/todo/utils/jwt_utils.py b/todo/utils/jwt_utils.py index 157b4f6c..d7fbb423 100644 --- a/todo/utils/jwt_utils.py +++ b/todo/utils/jwt_utils.py @@ -4,7 +4,6 @@ def verify_jwt_token(token: str) -> dict: - if not token or not token.strip(): raise TokenMissingError() diff --git a/todo/views/auth.py b/todo/views/auth.py index 76deb331..1e02e2d1 100644 --- a/todo/views/auth.py +++ b/todo/views/auth.py @@ -31,14 +31,13 @@ def get(self, request: Request): request.session["oauth_state"] = state if request.headers.get("Accept") == "application/json" or request.query_params.get("format") == "json": - return Response({ - "statusCode": status.HTTP_200_OK, - "message": "Google OAuth URL generated successfully", - "data": { - "authUrl": auth_url, - "state": state + return Response( + { + "statusCode": status.HTTP_200_OK, + "message": "Google OAuth URL generated successfully", + "data": {"authUrl": auth_url, "state": state}, } - }) + ) return HttpResponseRedirect(auth_url) @@ -90,22 +89,24 @@ def _handle_callback_directly(self, code, request): ) if wants_json: - response = Response({ - "statusCode": status.HTTP_200_OK, - "message": AppMessages.GOOGLE_LOGIN_SUCCESS, - "data": { - "user": { - "id": str(user.id), - "name": user.name, - "email": user.email_id, - "google_id": user.google_id, + response = Response( + { + "statusCode": status.HTTP_200_OK, + "message": AppMessages.GOOGLE_LOGIN_SUCCESS, + "data": { + "user": { + "id": str(user.id), + "name": user.name, + "email": user.email_id, + "google_id": user.google_id, + }, + "tokens": { + "access_token_expires_in": tokens["expires_in"], + "refresh_token_expires_in": settings.GOOGLE_JWT["REFRESH_TOKEN_LIFETIME"], + }, }, - "tokens": { - "access_token_expires_in": tokens["expires_in"], - "refresh_token_expires_in": settings.GOOGLE_JWT["REFRESH_TOKEN_LIFETIME"] - } } - }) + ) else: response = HttpResponse(f""" @@ -286,19 +287,21 @@ def get(self, request: Request): except Exception as e: raise GoogleTokenInvalidError(str(e)) - return Response({ - "statusCode": status.HTTP_200_OK, - "message": "Authentication status retrieved successfully", - "data": { - "authenticated": True, - "user": { - "id": str(user.id), - "email": user.email_id, - "name": user.name, - "google_id": user.google_id, - } + return Response( + { + "statusCode": status.HTTP_200_OK, + "message": "Authentication status retrieved successfully", + "data": { + "authenticated": True, + "user": { + "id": str(user.id), + "email": user.email_id, + "name": user.name, + "google_id": user.google_id, + }, + }, } - }) + ) class GoogleRefreshView(APIView): @@ -318,13 +321,9 @@ def get(self, request: Request): } new_access_token = generate_google_access_token(user_data) - response = Response({ - "statusCode": status.HTTP_200_OK, - "message": AppMessages.TOKEN_REFRESHED, - "data": { - "success": True - } - }) + response = Response( + {"statusCode": status.HTTP_200_OK, "message": AppMessages.TOKEN_REFRESHED, "data": {"success": True}} + ) config = self._get_cookie_config() response.set_cookie( @@ -362,13 +361,13 @@ def _handle_logout(self, request: Request): ) if wants_json: - response = Response({ - "statusCode": status.HTTP_200_OK, - "message": AppMessages.GOOGLE_LOGOUT_SUCCESS, - "data": { - "success": True + response = Response( + { + "statusCode": status.HTTP_200_OK, + "message": AppMessages.GOOGLE_LOGOUT_SUCCESS, + "data": {"success": True}, } - }) + ) else: redirect_url = redirect_url or "/" response = HttpResponseRedirect(redirect_url)