diff --git a/backend/api/api_v1/projects/list_views.py b/backend/api/api_v1/projects/list_views.py index 749423c..76491a5 100644 --- a/backend/api/api_v1/projects/list_views.py +++ b/backend/api/api_v1/projects/list_views.py @@ -12,7 +12,6 @@ ProjectWithRenderCreate, ProjectWithRenderResponse, ) -from services.projects.project_pagination import ProjectPaginationService router = APIRouter() @@ -26,7 +25,7 @@ ], ) async def get_public_projects( - project_pagination_service: ProjectPaginationService, + project_pagination_service: ProjectPaginationServiceDep, size: PaginationSizeDep = 10, page: PaginationPageDep = 1, ) -> ProjectResponseList: diff --git a/backend/api/api_v1/users.py b/backend/api/api_v1/users.py index afd547a..5bdba2b 100644 --- a/backend/api/api_v1/users.py +++ b/backend/api/api_v1/users.py @@ -36,7 +36,7 @@ async def update_current_user_profile( @router.delete( "/about-me", response_model=None, - status_code=status.HTTP_200_OK, + status_code=status.HTTP_204_NO_CONTENT, ) async def delete_current_user_profile( user_id: AuthUserByAccessTokenDep, @@ -50,7 +50,7 @@ async def delete_current_user_profile( response_model=UserResponse, status_code=status.HTTP_200_OK, ) -async def get_user_by_id( +async def get_user_profile( user_id: int, user_service: UserServiceDep, ) -> UserResponse: diff --git a/backend/api/exception_handlers.py b/backend/api/exception_handlers.py index c81191f..ea30e12 100644 --- a/backend/api/exception_handlers.py +++ b/backend/api/exception_handlers.py @@ -40,7 +40,7 @@ def exception_handler( status_code=status.HTTP_404_NOT_FOUND, ) conflict_error_exception_handler = exception_handler_factory( - status_code=status.HTTP_400_BAD_REQUEST, + status_code=status.HTTP_409_CONFLICT, ) authentication_error_exception_handler = exception_handler_factory( status_code=status.HTTP_403_FORBIDDEN, diff --git a/backend/core/interfaces/services.py b/backend/core/interfaces/services.py index adc70b9..463e14a 100644 --- a/backend/core/interfaces/services.py +++ b/backend/core/interfaces/services.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod from typing import BinaryIO +from infrastructure.database.models import Project from schemas.file import FileResponse @@ -18,3 +19,29 @@ async def upload( """ Метод для загрузки файла. """ + + +class AbstractProjectAccessValidatorService(ABC): + """ + Интерфейс для сервиса проверки прав доступа к проекту. + """ + + @abstractmethod + async def validate_access_to_get_project( + self, + project_id: int, + user_id: int, + ) -> Project: + """ + Метод для проверки прав доступа на просмотр проекта. + """ + + @abstractmethod + async def validate_access_to_change_project( + self, + project_id: int, + user_id: int, + ) -> Project: + """ + Метод для проверки прав доступа на изменение данных проекта. + """ diff --git a/backend/dependencies/services.py b/backend/dependencies/services.py index 0f26985..1526427 100644 --- a/backend/dependencies/services.py +++ b/backend/dependencies/services.py @@ -7,8 +7,8 @@ from infrastructure.minio.client import MinioClient from services.auth import AuthService from services.file_uploader import FileUploader -from services.projects.access_validator import ProjectAccessValidatorService from services.projects.project import ProjectService +from services.projects.project_access_validator import ProjectAccessValidatorService from services.projects.project_pagination import ProjectPaginationService from services.tag import TagService from services.user import UserService diff --git a/backend/services/projects/project.py b/backend/services/projects/project.py index a9f7fe9..d713787 100644 --- a/backend/services/projects/project.py +++ b/backend/services/projects/project.py @@ -1,6 +1,7 @@ from core.config.application import settings from core.exceptions.file import FileIdNotFoundError from core.interfaces.clients import AbstractS3Client, AbstractUnitOfWorkClient +from core.interfaces.services import AbstractProjectAccessValidatorService from core.logging import get_logger from infrastructure.database.repositories import ( FileRepository, @@ -22,8 +23,6 @@ ) from schemas.render import RenderCreate -from services.projects.access_validator import ProjectAccessValidatorService - logger = get_logger(__name__) @@ -38,7 +37,7 @@ class ProjectService: def __init__( self, unit_of_work: AbstractUnitOfWorkClient, - project_access_validator: ProjectAccessValidatorService, + project_access_validator: AbstractProjectAccessValidatorService, ) -> None: self.project_access_validator = project_access_validator self.unit_of_work = unit_of_work diff --git a/backend/services/projects/access_validator.py b/backend/services/projects/project_access_validator.py similarity index 92% rename from backend/services/projects/access_validator.py rename to backend/services/projects/project_access_validator.py index 16bef15..f26ddd5 100644 --- a/backend/services/projects/access_validator.py +++ b/backend/services/projects/project_access_validator.py @@ -3,13 +3,14 @@ from core.exceptions.project import ProjectIdNotFoundError from core.exceptions.user import UserIdNotFoundError from core.interfaces.clients import AbstractUnitOfWorkClient +from core.interfaces.services import AbstractProjectAccessValidatorService from infrastructure.database.models import Project from infrastructure.database.repositories import ( ProjectRepository, ) -class ProjectAccessValidatorService: +class ProjectAccessValidatorService(AbstractProjectAccessValidatorService): def __init__( self, unit_of_work: AbstractUnitOfWorkClient, diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 9b3c668..b6c08a2 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -6,8 +6,8 @@ from infrastructure.minio.client import MinioClient from services.auth import AuthService from services.file_uploader import FileUploader -from services.projects.access_validator import ProjectAccessValidatorService from services.projects.project import ProjectService +from services.projects.project_access_validator import ProjectAccessValidatorService from services.projects.project_pagination import ProjectPaginationService from services.projects.render_generate_handler import RenderGenerateHandlerService from services.tag import TagService diff --git a/backend/tests/test_api/__init__.py b/backend/tests/test_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_api/conftest.py b/backend/tests/test_api/conftest.py new file mode 100644 index 0000000..68533c2 --- /dev/null +++ b/backend/tests/test_api/conftest.py @@ -0,0 +1,135 @@ +from collections.abc import Generator +from datetime import datetime +from unittest.mock import AsyncMock + +import pytest +from application_factory import create_app +from core.constants import BEARER_TOKEN_TYPE +from dependencies.auth import get_auth_user_by_access_token +from fastapi import FastAPI +from fastapi.testclient import TestClient +from schemas.file import FileResponse +from schemas.tag import TagResponse, TagResponseList +from schemas.token import TokenInfo +from schemas.user import UserFullResponse, UserResponse +from services.auth import AuthService +from services.file_uploader import FileUploader +from services.tag import TagService +from services.user import UserService + + +@pytest.fixture(scope="session") +def application() -> FastAPI: + application = create_app() + return application + + +@pytest.fixture(scope="session") +def client(application: FastAPI) -> TestClient: + return TestClient(application) + + +@pytest.fixture(scope="function") +def auth_client(application: FastAPI) -> Generator[TestClient]: + user_id = 1 + application.dependency_overrides[get_auth_user_by_access_token] = lambda: user_id + yield TestClient(application) + application.dependency_overrides.pop(get_auth_user_by_access_token) + + +@pytest.fixture(scope="session") +def file_uploader() -> FileUploader: + file_uploader = AsyncMock() + file_response = FileResponse( + id=1, + name="test_name", + size=1000, + ) + file_uploader.upload = AsyncMock( + return_value=file_response, + ) + return file_uploader + + +@pytest.fixture(scope="session") +def auth_service() -> AuthService: + auth_service = AsyncMock() + register_token = TokenInfo( + access_token="access_token", + refresh_token="refresh_token", + token_type=BEARER_TOKEN_TYPE, + ) + login_token = TokenInfo( + access_token="login_access_token", + refresh_token="login_refresh_token", + token_type=BEARER_TOKEN_TYPE, + ) + refresh_access_token_response = TokenInfo( + access_token="access_token", + token_type=BEARER_TOKEN_TYPE, + ) + + auth_service.register = AsyncMock( + return_value=register_token, + ) + auth_service.authenticate_user = AsyncMock( + return_value=login_token, + ) + auth_service.refresh_access_token = AsyncMock( + return_value=refresh_access_token_response, + ) + return auth_service + + +@pytest.fixture(scope="session") +def user_service() -> UserService: + user_service = AsyncMock() + user_full_response = UserFullResponse( + id=1, + registration_date=datetime( + year=2025, + month=1, + day=1, + hour=0, + minute=0, + second=0, + ), + surname="surname", + name="name", + username="username", + email="email@email.com", + ) + user_response = UserResponse( + id=1, + registration_date=datetime( + year=2025, + month=1, + day=1, + hour=0, + minute=0, + second=0, + ), + surname="surname", + name="name", + username="username", + ) + user_service.get_profile_by_id = AsyncMock(return_value=user_full_response) + user_service.update_user = AsyncMock(return_value=user_response) + user_service.delete_by_id = AsyncMock(return_value=None) + user_service.get_by_id = AsyncMock(return_value=user_response) + return user_service + + +@pytest.fixture(scope="function") +def tag_service() -> TagService: + tag_service = AsyncMock() + tag_response = TagResponse( + id=1, + project_id=1, + name="tag_name", + ) + tag_response_list = TagResponseList(tag_list=[tag_response]) + tag_service.create_tag = AsyncMock(return_value=tag_response) + tag_service.delete_by_id = AsyncMock(return_value=None) + tag_service.get_project_tags = AsyncMock(return_value=tag_response_list) + return tag_service diff --git a/backend/tests/test_api/test_api_v1/__init__.py b/backend/tests/test_api/test_api_v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_api/test_api_v1/test_auth.py b/backend/tests/test_api/test_api_v1/test_auth.py new file mode 100644 index 0000000..b5ff8ab --- /dev/null +++ b/backend/tests/test_api/test_api_v1/test_auth.py @@ -0,0 +1,172 @@ +import pytest +from core.exceptions.auth import InvalidPasswordError +from core.exceptions.base import ConflictError +from core.exceptions.user import ( + UserEmailAlreadyExistsError, + UserIdNotFoundError, + UserUsernameAlreadyExistsError, + UserUsernameNotFoundError, +) +from dependencies.auth import get_auth_user_by_refresh_token, get_login_request +from dependencies.services import get_auth_service +from fastapi import FastAPI, status +from fastapi.testclient import TestClient +from schemas.auth import LoginRequest, RegisterRequest +from services.auth import AuthService + +from tests.test_api.utils import get_result_json + + +@pytest.fixture(scope="function") +def register_request() -> RegisterRequest: + register_request = RegisterRequest( + surname="surname", + name="name", + username="username", + email="email@email.com", + password="password", + ) + return register_request + + +@pytest.fixture(scope="function") +def login_request() -> LoginRequest: + login_request = LoginRequest( + username="username", + password="password", + ) + return login_request + + +class TestRegisterEndpoint: + url = "/api/v1/auth/register" + + def test_register_valid( + self, + register_request: RegisterRequest, + application: FastAPI, + client: TestClient, + auth_service: AuthService, + ) -> None: + application.dependency_overrides[get_auth_service] = lambda: auth_service + response = client.post( + self.url, + json=register_request.model_dump(), + ) + response_data = response.json() + assert response.status_code == status.HTTP_201_CREATED + assert response_data == get_result_json(auth_service, "register") + + @pytest.mark.parametrize( + "conflict_error", + [ + UserUsernameAlreadyExistsError("existed_username"), + UserEmailAlreadyExistsError("existed_email@email.com"), + ], + ) + def test_register_conflict_error( + self, + register_request: RegisterRequest, + conflict_error: ConflictError, + application: FastAPI, + client: TestClient, + auth_service: AuthService, + ) -> None: + auth_service.register.side_effect = conflict_error + application.dependency_overrides[get_auth_service] = lambda: auth_service + response = client.post( + self.url, + json=register_request.model_dump(), + ) + response_data = response.json() + assert response.status_code == status.HTTP_409_CONFLICT + assert response_data["message"] == conflict_error.detail + + +class TestLoginEndpoint: + url = "/api/v1/auth/login" + + def test_login_valid( + self, + login_request: LoginRequest, + application: FastAPI, + client: TestClient, + auth_service: AuthService, + ) -> None: + application.dependency_overrides[get_auth_service] = lambda: auth_service + application.dependency_overrides[get_login_request] = lambda: login_request + response = client.post(self.url) + response_data = response.json() + assert response.status_code == status.HTTP_200_OK + assert response_data == get_result_json(auth_service, "authenticate_user") + + def test_login_username_not_found( + self, + login_request: LoginRequest, + application: FastAPI, + client: TestClient, + auth_service: AuthService, + ) -> None: + application.dependency_overrides[get_auth_service] = lambda: auth_service + application.dependency_overrides[get_login_request] = lambda: login_request + username_not_found_error = UserUsernameNotFoundError(login_request.username) + auth_service.authenticate_user.side_effect = username_not_found_error + response = client.post(self.url) + response_data = response.json() + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response_data["message"] == username_not_found_error.detail + + def test_login_invalid_password( + self, + login_request: LoginRequest, + application: FastAPI, + client: TestClient, + auth_service: AuthService, + ) -> None: + application.dependency_overrides[get_auth_service] = lambda: auth_service + application.dependency_overrides[get_login_request] = lambda: login_request + invalid_password_error = InvalidPasswordError(login_request.password) + auth_service.authenticate_user.side_effect = invalid_password_error + response = client.post(self.url) + response_data = response.json() + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response_data["message"] == invalid_password_error.detail + + +class TestRefreshAccessTokenEndpoint: + url = "/api/v1/auth/refresh" + + def test_refresh_access_token_valid( + self, + application: FastAPI, + client: TestClient, + auth_service: AuthService, + ) -> None: + application.dependency_overrides[get_auth_service] = lambda: auth_service + application.dependency_overrides[get_auth_user_by_refresh_token] = lambda: 1 + response = client.get(self.url) + response_data = response.json() + assert response.status_code == status.HTTP_200_OK + assert response_data == get_result_json( + auth_service, + "refresh_access_token", + exclude_unset=True, + ) + + def test_refresh_access_token_user_not_found( + self, + application: FastAPI, + client: TestClient, + auth_service: AuthService, + ) -> None: + user_id = 1 + application.dependency_overrides[get_auth_service] = lambda: auth_service + application.dependency_overrides[get_auth_user_by_refresh_token] = ( + lambda: user_id + ) + user_not_found_error = UserIdNotFoundError(user_id) + auth_service.refresh_access_token.side_effect = user_not_found_error + response = client.get(self.url) + response_data = response.json() + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response_data["message"] == user_not_found_error.detail diff --git a/backend/tests/test_api/test_api_v1/test_tags/__init__.py b/backend/tests/test_api/test_api_v1/test_tags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_api/test_api_v1/test_tags/test_details_views.py b/backend/tests/test_api/test_api_v1/test_tags/test_details_views.py new file mode 100644 index 0000000..908eb55 --- /dev/null +++ b/backend/tests/test_api/test_api_v1/test_tags/test_details_views.py @@ -0,0 +1,112 @@ +import pytest +from core.exceptions.auth import PermissionDeniedError +from core.exceptions.base import NotFoundError +from core.exceptions.project import ProjectIdNotFoundError +from core.exceptions.tag import TagIdNotFoundError +from core.exceptions.user import UserIdNotFoundError +from dependencies.services import get_tag_service +from fastapi import FastAPI, status +from fastapi.testclient import TestClient +from services.tag import TagService + +from tests.test_api.utils import get_result_json + + +class TestGetProjectTagsEndpoint: + url = "/api/v1/tags/project/{project_id}" + + def test_get_project_tags_valid( + self, + application: FastAPI, + auth_client: TestClient, + tag_service: TagService, + ) -> None: + application.dependency_overrides[get_tag_service] = lambda: tag_service + url = self.url.format(project_id=1) + response = auth_client.get(url) + response_data = response.json() + assert response.status_code == status.HTTP_200_OK + assert response_data == get_result_json(tag_service, "get_project_tags") + + @pytest.mark.parametrize( + "not_found_error", + [ + ProjectIdNotFoundError(1), + UserIdNotFoundError(1), + ], + ) + def test_get_project_tags_not_found_error( + self, + application: FastAPI, + auth_client: TestClient, + tag_service: TagService, + not_found_error: NotFoundError, + ) -> None: + application.dependency_overrides[get_tag_service] = lambda: tag_service + tag_service.get_project_tags.side_effect = not_found_error + url = self.url.format(project_id=1) + response = auth_client.get(url) + response_data = response.json() + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response_data["message"] == not_found_error.detail + + def test_get_project_tags_permission_denied_error( + self, + application: FastAPI, + auth_client: TestClient, + tag_service: TagService, + ) -> None: + application.dependency_overrides[get_tag_service] = lambda: tag_service + tag_service.get_project_tags.side_effect = PermissionDeniedError + url = self.url.format(project_id=1) + response = auth_client.get(url) + response_data = response.json() + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response_data["message"] == PermissionDeniedError().detail + + +class TestDeleteProjectTagEndpoint: + url = "/api/v1/tags/{tag_id}" + + def test_delete_project_tag_valid( + self, + application: FastAPI, + auth_client: TestClient, + tag_service: TagService, + ) -> None: + application.dependency_overrides[get_tag_service] = lambda: tag_service + url = self.url.format(tag_id=1) + response = auth_client.delete(url) + assert response.status_code == status.HTTP_204_NO_CONTENT + + @pytest.mark.parametrize( + "not_found_error", + [ + TagIdNotFoundError(1), + UserIdNotFoundError(1), + ], + ) + def test_delete_project_tag_not_found_error( + self, + application: FastAPI, + auth_client: TestClient, + tag_service: TagService, + not_found_error: NotFoundError, + ) -> None: + application.dependency_overrides[get_tag_service] = lambda: tag_service + tag_service.delete_by_id.side_effect = not_found_error + url = self.url.format(tag_id=1) + response = auth_client.delete(url) + assert response.status_code == status.HTTP_404_NOT_FOUND + + def test_delete_project_tag_permission_denied_error( + self, + application: FastAPI, + auth_client: TestClient, + tag_service: TagService, + ) -> None: + application.dependency_overrides[get_tag_service] = lambda: tag_service + tag_service.delete_by_id.side_effect = PermissionDeniedError + url = self.url.format(tag_id=1) + response = auth_client.delete(url) + assert response.status_code == status.HTTP_401_UNAUTHORIZED diff --git a/backend/tests/test_api/test_api_v1/test_tags/test_list_views.py b/backend/tests/test_api/test_api_v1/test_tags/test_list_views.py new file mode 100644 index 0000000..fcb49d8 --- /dev/null +++ b/backend/tests/test_api/test_api_v1/test_tags/test_list_views.py @@ -0,0 +1,69 @@ +import pytest +from core.exceptions.auth import PermissionDeniedError +from core.exceptions.base import NotFoundError +from core.exceptions.project import ProjectIdNotFoundError +from core.exceptions.user import UserIdNotFoundError +from dependencies.services import get_tag_service +from fastapi import FastAPI, status +from fastapi.testclient import TestClient +from schemas.tag import TagCreate +from services.tag import TagService + + +@pytest.fixture(scope="function") +def create_tag_data() -> TagCreate: + return TagCreate( + name="tag_name", + project_id=1, + ) + + +class TestCreateTagEndpoint: + url = "/api/v1/tags/create" + + def test_create_tag_valid( + self, + application: FastAPI, + auth_client: TestClient, + tag_service: TagService, + create_tag_data: TagCreate, + ) -> None: + application.dependency_overrides[get_tag_service] = lambda: tag_service + response = auth_client.post(self.url, json=create_tag_data.model_dump()) + assert response.status_code == status.HTTP_201_CREATED + + @pytest.mark.parametrize( + "not_found_error", + [ + ProjectIdNotFoundError(1), + UserIdNotFoundError(1), + ], + ) + def test_create_tag_not_found_error( + self, + application: FastAPI, + auth_client: TestClient, + tag_service: TagService, + not_found_error: NotFoundError, + create_tag_data: TagCreate, + ) -> None: + application.dependency_overrides[get_tag_service] = lambda: tag_service + tag_service.create_tag.side_effect = not_found_error + response = auth_client.post(self.url, json=create_tag_data.model_dump()) + response_data = response.json() + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response_data["message"] == not_found_error.detail + + def test_create_tag_permission_denied_error( + self, + application: FastAPI, + auth_client: TestClient, + tag_service: TagService, + create_tag_data: TagCreate, + ) -> None: + application.dependency_overrides[get_tag_service] = lambda: tag_service + tag_service.create_tag.side_effect = PermissionDeniedError + response = auth_client.post(self.url, json=create_tag_data.model_dump()) + response_data = response.json() + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response_data["message"] == PermissionDeniedError().detail diff --git a/backend/tests/test_api/test_api_v1/test_upload_file.py b/backend/tests/test_api/test_api_v1/test_upload_file.py new file mode 100644 index 0000000..a1aa213 --- /dev/null +++ b/backend/tests/test_api/test_api_v1/test_upload_file.py @@ -0,0 +1,25 @@ +from dependencies.services import get_input_file_uploader +from fastapi import FastAPI, status +from fastapi.testclient import TestClient +from services.file_uploader import FileUploader + +from tests.test_api.utils import get_result_json + + +class TestUploadFileEndpoint: + url = "/api/v1/files/upload" + + def test_upload_file_valid( + self, + application: FastAPI, + auth_client: TestClient, + file_uploader: FileUploader, + ) -> None: + application.dependency_overrides[get_input_file_uploader] = ( + lambda: file_uploader + ) + files = {"uploaded_file": ("test_file.txt", b"test file content", "text/plain")} + response = auth_client.post(self.url, files=files) + response_data = response.json() + assert response.status_code == status.HTTP_201_CREATED + assert response_data == get_result_json(file_uploader, "upload") diff --git a/backend/tests/test_api/test_api_v1/test_users.py b/backend/tests/test_api/test_api_v1/test_users.py new file mode 100644 index 0000000..0a57de2 --- /dev/null +++ b/backend/tests/test_api/test_api_v1/test_users.py @@ -0,0 +1,155 @@ +from datetime import datetime + +import pytest +from core.exceptions.user import UserIdNotFoundError +from dependencies.services import get_user_service +from fastapi import FastAPI, status +from fastapi.testclient import TestClient +from schemas.user import UserUpdate +from services.user import UserService + +from tests.test_api.utils import get_result_json + + +@pytest.fixture(scope="function") +def update_user_data() -> UserUpdate: + return UserUpdate( + surname="surname", + name="name", + username="username", + email="email@email.com", + ) + + +class TestGetCurrentUserProfileEndpoint: + url = "/api/v1/users/about-me" + + def test_get_current_user_profile_valid( + self, + application: FastAPI, + auth_client: TestClient, + user_service: UserService, + ) -> None: + application.dependency_overrides[get_user_service] = lambda: user_service + response = auth_client.get(self.url) + response_data = response.json() + assert response.status_code == status.HTTP_200_OK + expected_response_data = get_result_json(user_service, "get_profile_by_id") + response_data["registration_date"] = datetime.fromisoformat( + response_data["registration_date"], + ) + assert response_data == expected_response_data + + def test_get_current_user_profile_not_found( + self, + application: FastAPI, + auth_client: TestClient, + user_service: UserService, + ) -> None: + user_not_found_error = UserIdNotFoundError(1) + application.dependency_overrides[get_user_service] = lambda: user_service + user_service.get_profile_by_id.side_effect = user_not_found_error + response = auth_client.get(self.url) + response_data = response.json() + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response_data["message"] == user_not_found_error.detail + + +class TestUpdateCurrentUserProfileEndpoint: + url = "/api/v1/users/about-me" + + def test_update_current_user_profile_valid( + self, + application: FastAPI, + auth_client: TestClient, + user_service: UserService, + update_user_data: UserUpdate, + ) -> None: + application.dependency_overrides[get_user_service] = lambda: user_service + response = auth_client.put(self.url, json=update_user_data.model_dump()) + response_data = response.json() + assert response.status_code == status.HTTP_200_OK + expected_response_data = get_result_json(user_service, "update_user") + response_data["registration_date"] = datetime.fromisoformat( + response_data["registration_date"], + ) + assert response_data == expected_response_data + + def test_update_current_user_profile_not_found( + self, + application: FastAPI, + auth_client: TestClient, + user_service: UserService, + update_user_data: UserUpdate, + ) -> None: + user_not_found_error = UserIdNotFoundError(1) + application.dependency_overrides[get_user_service] = lambda: user_service + user_service.update_user.side_effect = user_not_found_error + response = auth_client.put(self.url, json=update_user_data.model_dump()) + response_data = response.json() + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response_data["message"] == user_not_found_error.detail + + +class TestDeleteCurrentUserProfileEndpoint: + url = "/api/v1/users/about-me" + + def test_delete_current_user_profile_valid( + self, + application: FastAPI, + auth_client: TestClient, + user_service: UserService, + ) -> None: + application.dependency_overrides[get_user_service] = lambda: user_service + response = auth_client.delete(self.url) + assert response.status_code == status.HTTP_204_NO_CONTENT + + def test_delete_current_user_profile_not_found( + self, + application: FastAPI, + auth_client: TestClient, + user_service: UserService, + ) -> None: + user_not_found_error = UserIdNotFoundError(1) + application.dependency_overrides[get_user_service] = lambda: user_service + user_service.delete_by_id.side_effect = user_not_found_error + response = auth_client.delete(self.url) + response_data = response.json() + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response_data["message"] == user_not_found_error.detail + + +class TestGetUserProfileEndpoint: + url = "/api/v1/users/{user_id}" + + def test_get_user_profile_valid( + self, + application: FastAPI, + auth_client: TestClient, + user_service: UserService, + ) -> None: + application.dependency_overrides[get_user_service] = lambda: user_service + url = self.url.format(user_id=1) + response = auth_client.get(url) + response_data = response.json() + assert response.status_code == status.HTTP_200_OK + expected_response_data = get_result_json(user_service, "get_by_id") + response_data["registration_date"] = datetime.fromisoformat( + response_data["registration_date"], + ) + assert response_data == expected_response_data + + def test_get_user_profile_not_found( + self, + application: FastAPI, + auth_client: TestClient, + user_service: UserService, + ) -> None: + user_not_found_error = UserIdNotFoundError(1) + application.dependency_overrides[get_user_service] = lambda: user_service + user_service.get_by_id.side_effect = user_not_found_error + url = self.url.format(user_id=1) + response = auth_client.get(url) + response_data = response.json() + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response_data["message"] == user_not_found_error.detail diff --git a/backend/tests/test_api/test_main_views.py b/backend/tests/test_api/test_main_views.py new file mode 100644 index 0000000..6a72a21 --- /dev/null +++ b/backend/tests/test_api/test_main_views.py @@ -0,0 +1,31 @@ +from fastapi import status +from fastapi.testclient import TestClient + + +class TestReadRootEndpoint: + url = "/" + + def test_read_root(self, client: TestClient) -> None: + name = "test_name" + response = client.get(self.url, params={"name": name}) + response_data = response.json() + assert response.status_code == status.HTTP_200_OK + assert name in response_data["message"] + assert response_data.get("docs_url") is not None + + def test_read_root_default_query_params(self, client: TestClient) -> None: + response = client.get(self.url) + response_data = response.json() + assert response.status_code == status.HTTP_200_OK + assert response_data.get("message") is not None + assert response_data.get("docs_url") is not None + + +class TestCheckHealthEndpoint: + url = "/health" + + def test_check_health(self, client: TestClient) -> None: + response = client.get(self.url) + response_data = response.json() + assert response.status_code == status.HTTP_200_OK + assert response_data["status"] == "ok" diff --git a/backend/tests/test_api/utils.py b/backend/tests/test_api/utils.py new file mode 100644 index 0000000..8c17ba3 --- /dev/null +++ b/backend/tests/test_api/utils.py @@ -0,0 +1,11 @@ +from typing import Any + + +def get_result_json( + instance: Any, + method: str, + exclude_unset: bool = False, +) -> dict: + return getattr(instance, method).return_value.model_dump( + exclude_unset=exclude_unset, + )