diff --git a/backend/tests/test_api/conftest.py b/backend/tests/test_api/conftest.py index 68533c2..4d316f0 100644 --- a/backend/tests/test_api/conftest.py +++ b/backend/tests/test_api/conftest.py @@ -4,16 +4,33 @@ import pytest from application_factory import create_app -from core.constants import BEARER_TOKEN_TYPE +from core.constants import BEARER_TOKEN_TYPE, ProjectVisibility, RenderStatus 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.project import ( + ProjectBase, + ProjectFullResponse, + ProjectResponse, + ProjectResponseList, + ProjectWithRenderFileFullResponse, + ProjectWithRenderFileResponse, + ProjectWithRenderResponse, +) +from schemas.render import ( + RenderBase, + RenderFullResponse, + RenderResponse, + RenderWithFileFullResponse, + RenderWithFileResponse, +) 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.projects.project_pagination import ProjectPaginationService from services.tag import TagService from services.user import UserService @@ -120,6 +137,166 @@ def user_service() -> UserService: return user_service +@pytest.fixture(scope="function") +def render_base() -> RenderBase: + return RenderBase( + width=1000, + height=1000, + samples=100, + denoiser=True, + gpu=True, + ) + + +@pytest.fixture(scope="function") +def render_response(render_base: RenderBase) -> RenderResponse: + return RenderResponse( + id=1, + file_id=None, + **render_base.model_dump(), + ) + + +@pytest.fixture(scope="function") +def render_full_response(render_response: RenderResponse) -> RenderFullResponse: + return RenderFullResponse( + url=None, + **render_response.model_dump(), + ) + + +@pytest.fixture(scope="function") +def render_with_file_full_response( + render_full_response: RenderFullResponse, +) -> RenderWithFileFullResponse: + return RenderWithFileFullResponse( + file=None, + **render_full_response.model_dump(), + ) + + +@pytest.fixture(scope="function") +def project_base() -> ProjectBase: + return ProjectBase( + name="project_name", + description="description", + source_file_id=1, + ) + + +@pytest.fixture(scope="function") +def project_response( + project_base: ProjectBase, +) -> ProjectResponse: + return ProjectResponse( + visibility=ProjectVisibility.public, + status=RenderStatus.completed, + user_id=1, + render_id=None, + id=1, + **project_base.model_dump(), + ) + + +@pytest.fixture(scope="function") +def project_full_response( + project_response: ProjectResponse, +) -> ProjectFullResponse: + return ProjectFullResponse( + url="test_url", + **project_response.model_dump(), + ) + + +@pytest.fixture(scope="function") +def project_with_render_file_full_response( + project_full_response: ProjectFullResponse, + render_with_file_full_response: RenderWithFileFullResponse, +) -> ProjectWithRenderFileFullResponse: + return ProjectWithRenderFileFullResponse( + render=render_with_file_full_response, + **project_full_response.model_dump(), + ) + + +@pytest.fixture(scope="function") +def project_with_render_response( + render_response: RenderResponse, + project_response: ProjectResponse, +) -> ProjectWithRenderResponse: + return ProjectWithRenderResponse( + render=render_response, + **project_response.model_dump(), + ) + + +@pytest.fixture(scope="function") +def render_with_file_response( + render_response: RenderResponse, +) -> RenderWithFileResponse: + return RenderWithFileResponse( + file=None, + **render_response.model_dump(), + ) + + +@pytest.fixture(scope="function") +def project_with_render_file_response( + project_response: ProjectResponse, + render_with_file_response: RenderWithFileResponse, +) -> ProjectWithRenderFileResponse: + return ProjectWithRenderFileResponse( + render=render_with_file_response, + **project_response.model_dump(), + ) + + +@pytest.fixture(scope="function") +def project_service( + project_with_render_file_full_response: ProjectWithRenderFileFullResponse, + project_with_render_response: ProjectWithRenderResponse, + project_with_render_file_response: ProjectWithRenderFileResponse, +) -> TagService: + project_service = AsyncMock() + project_service.get_by_id = AsyncMock( + return_value=project_with_render_file_full_response, + ) + project_service.create_project = AsyncMock( + return_value=project_with_render_response, + ) + project_service.partial_update_project = AsyncMock( + return_value=project_with_render_file_response, + ) + project_service.delete_by_id = AsyncMock(return_value=None) + return project_service + + +@pytest.fixture(scope="function") +def project_response_list(project_response: ProjectResponse) -> ProjectResponseList: + return ProjectResponseList( + project_list=[project_response], + size=10, + page=1, + ) + + +@pytest.fixture(scope="function") +def project_pagination_service( + project_response_list: ProjectResponseList, +) -> ProjectPaginationService: + project_pagination_service = AsyncMock() + project_pagination_service.get_user_projects = AsyncMock( + return_value=project_response_list, + ) + project_pagination_service.get_public_projects = AsyncMock( + return_value=project_response_list, + ) + project_pagination_service.get_user_public_projects = AsyncMock( + return_value=project_response_list, + ) + return project_pagination_service + + @pytest.fixture(scope="function") def tag_service() -> TagService: tag_service = AsyncMock() diff --git a/backend/tests/test_api/test_api_v1/test_projects/__init__.py b/backend/tests/test_api/test_api_v1/test_projects/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tests/test_api/test_api_v1/test_projects/test_details_views.py b/backend/tests/test_api/test_api_v1/test_projects/test_details_views.py new file mode 100644 index 0000000..7feeb2d --- /dev/null +++ b/backend/tests/test_api/test_api_v1/test_projects/test_details_views.py @@ -0,0 +1,164 @@ +import pytest +from core.exceptions.auth import PermissionDeniedError +from core.exceptions.base import NotFoundError +from core.exceptions.file import FileIdNotFoundError +from core.exceptions.project import ProjectIdNotFoundError +from core.exceptions.user import UserIdNotFoundError +from dependencies.services import get_project_service +from fastapi import FastAPI, status +from fastapi.testclient import TestClient +from schemas.project import ProjectPartialUpdate +from services.projects.project import ProjectService + +from tests.test_api.utils import get_result_json + + +@pytest.fixture(scope="function") +def partial_update_project_data() -> ProjectPartialUpdate: + return ProjectPartialUpdate( + name="project_name", + description="description", + ) + + +class TestGetProjectEndpoint: + url = "/api/v1/projects/{project_id}" + + def test_get_project_valid( + self, + application: FastAPI, + auth_client: TestClient, + project_service: ProjectService, + ) -> None: + application.dependency_overrides[get_project_service] = lambda: project_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(project_service, "get_by_id") + + @pytest.mark.parametrize( + "not_found_error", + [ + ProjectIdNotFoundError(1), + UserIdNotFoundError(1), + ], + ) + def test_get_project_not_found_error( + self, + application: FastAPI, + auth_client: TestClient, + project_service: ProjectService, + not_found_error: NotFoundError, + ) -> None: + application.dependency_overrides[get_project_service] = lambda: project_service + project_service.get_by_id.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_permission_denied_error( + self, + application: FastAPI, + auth_client: TestClient, + project_service: ProjectService, + ) -> None: + application.dependency_overrides[get_project_service] = lambda: project_service + permission_denied_error = PermissionDeniedError() + project_service.get_by_id.side_effect = permission_denied_error + 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"] == permission_denied_error.detail + + +class TestPartialUpdateProjectEndpoint: + url = "/api/v1/projects/{project_id}" + + def test_partial_update_project_valid( + self, + application: FastAPI, + auth_client: TestClient, + project_service: ProjectService, + partial_update_project_data: ProjectPartialUpdate, + ) -> None: + application.dependency_overrides[get_project_service] = lambda: project_service + url = self.url.format(project_id=1) + response = auth_client.patch(url, json=partial_update_project_data.model_dump()) + response_data = response.json() + assert response.status_code == status.HTTP_200_OK + assert response_data == get_result_json( + project_service, + "partial_update_project", + ) + + def test_partial_update_project_file_not_found_error( + self, + application: FastAPI, + auth_client: TestClient, + project_service: ProjectService, + partial_update_project_data: ProjectPartialUpdate, + ) -> None: + file_not_found_error = FileIdNotFoundError(1) + application.dependency_overrides[get_project_service] = lambda: project_service + project_service.partial_update_project.side_effect = file_not_found_error + url = self.url.format(project_id=1) + response = auth_client.patch(url, json=partial_update_project_data.model_dump()) + response_data = response.json() + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response_data["message"] == file_not_found_error.detail + + +class TestDeleteProjectEndpoint: + url = "/api/v1/projects/{project_id}" + + def test_delete_project_valid( + self, + application: FastAPI, + auth_client: TestClient, + project_service: ProjectService, + ) -> None: + application.dependency_overrides[get_project_service] = lambda: project_service + url = self.url.format(project_id=1) + response = auth_client.delete(url) + assert response.status_code == status.HTTP_204_NO_CONTENT + + @pytest.mark.parametrize( + "not_found_error", + [ + ProjectIdNotFoundError(1), + UserIdNotFoundError(1), + ], + ) + def test_delete_project_not_found_error( + self, + application: FastAPI, + auth_client: TestClient, + project_service: ProjectService, + not_found_error: NotFoundError, + ) -> None: + application.dependency_overrides[get_project_service] = lambda: project_service + project_service.delete_by_id.side_effect = not_found_error + url = self.url.format(project_id=1) + response = auth_client.delete(url) + response_data = response.json() + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response_data["message"] == not_found_error.detail + + def test_delete_project_permission_denied_error( + self, + application: FastAPI, + auth_client: TestClient, + project_service: ProjectService, + ) -> None: + application.dependency_overrides[get_project_service] = lambda: project_service + permission_denied_error = PermissionDeniedError() + project_service.delete_by_id.side_effect = permission_denied_error + url = self.url.format(project_id=1) + response = auth_client.delete(url) + response_data = response.json() + assert response.status_code == status.HTTP_401_UNAUTHORIZED + assert response_data["message"] == permission_denied_error.detail diff --git a/backend/tests/test_api/test_api_v1/test_projects/test_list_views.py b/backend/tests/test_api/test_api_v1/test_projects/test_list_views.py new file mode 100644 index 0000000..02f48c3 --- /dev/null +++ b/backend/tests/test_api/test_api_v1/test_projects/test_list_views.py @@ -0,0 +1,188 @@ +import pytest +from core.constants import ProjectVisibility +from core.exceptions.file import FileIdNotFoundError +from core.exceptions.user import UserIdNotFoundError +from dependencies.services import get_project_pagination_service, get_project_service +from fastapi import FastAPI, status +from fastapi.testclient import TestClient +from schemas.project import ( + ProjectBase, + ProjectCreate, + ProjectWithRenderCreate, +) +from schemas.render import RenderBase, RenderCreatePayload, SunInfo +from services.projects.project import ProjectService +from services.projects.project_pagination import ProjectPaginationService + +from tests.test_api.utils import get_result_json + + +@pytest.fixture(scope="function") +def sun() -> SunInfo: + return SunInfo( + direction=[1.0, 1.0, 1.0], + color=[1.0, 1.0, 1.0], + exponent=1, + ) + + +@pytest.fixture(scope="function") +def render_create_payload(render_base: RenderBase, sun: SunInfo) -> RenderCreatePayload: + return RenderCreatePayload( + background=[1.0, 1.0, 1.0], + sun=sun, + **render_base.model_dump(), + ) + + +@pytest.fixture(scope="function") +def project_create_payload(project_base: ProjectBase) -> ProjectCreate: + return ProjectCreate( + visibility=ProjectVisibility.public, + **project_base.model_dump(), + ) + + +@pytest.fixture(scope="function") +def create_project_data( + render_create_payload: RenderCreatePayload, + project_create_payload: ProjectCreate, +) -> ProjectWithRenderCreate: + return ProjectWithRenderCreate( + render=render_create_payload, + project=project_create_payload, + ) + + +class TestCreateProjectEndpoint: + url = "/api/v1/projects/create" + + def test_create_project_valid( + self, + application: FastAPI, + auth_client: TestClient, + project_service: ProjectService, + create_project_data: ProjectWithRenderCreate, + ) -> None: + application.dependency_overrides[get_project_service] = lambda: project_service + response = auth_client.post(self.url, json=create_project_data.model_dump()) + response_data = response.json() + assert response.status_code == status.HTTP_201_CREATED + assert response_data == get_result_json( + project_service, + "create_project", + ) + + def test_create_project_file_not_found_error( + self, + application: FastAPI, + auth_client: TestClient, + project_service: ProjectService, + create_project_data: ProjectWithRenderCreate, + ) -> None: + application.dependency_overrides[get_project_service] = lambda: project_service + file_not_found_error = FileIdNotFoundError(1) + project_service.create_project.side_effect = file_not_found_error + response = auth_client.post(self.url, json=create_project_data.model_dump()) + response_data = response.json() + assert response.status_code == status.HTTP_404_NOT_FOUND + assert response_data["message"] == file_not_found_error.detail + + +class TestGetPublicProjects: + url = "/api/v1/projects/" + + def test_get_public_projects_valid( + self, + application: FastAPI, + auth_client: TestClient, + project_pagination_service: ProjectPaginationService, + ) -> None: + application.dependency_overrides[get_project_pagination_service] = ( + lambda: project_pagination_service + ) + response = auth_client.get(self.url) + response_data = response.json() + assert response.status_code == status.HTTP_200_OK + assert response_data == get_result_json( + project_pagination_service, + "get_public_projects", + ) + + +class TestGetCurrentUserProjects: + url = "/api/v1/projects/about-me" + + def test_get_current_user_projects_valid( + self, + application: FastAPI, + auth_client: TestClient, + project_pagination_service: ProjectPaginationService, + ) -> None: + application.dependency_overrides[get_project_pagination_service] = ( + lambda: project_pagination_service + ) + response = auth_client.get(self.url) + response_data = response.json() + assert response.status_code == status.HTTP_200_OK + assert response_data == get_result_json( + project_pagination_service, + "get_user_projects", + ) + + def test_get_current_user_projects_user_not_found_error( + self, + application: FastAPI, + auth_client: TestClient, + project_pagination_service: ProjectPaginationService, + ) -> None: + application.dependency_overrides[get_project_pagination_service] = ( + lambda: project_pagination_service + ) + user_not_found_error = UserIdNotFoundError(1) + project_pagination_service.get_user_projects.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 TestGetUserPublicProjects: + url = "/api/v1/projects/user/{user_id}" + + def test_get_user_public_projects_valid( + self, + application: FastAPI, + auth_client: TestClient, + project_pagination_service: ProjectPaginationService, + ) -> None: + application.dependency_overrides[get_project_pagination_service] = ( + lambda: project_pagination_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 + assert response_data == get_result_json( + project_pagination_service, + "get_user_public_projects", + ) + + def test_get_user_public_projects_user_not_found_error( + self, + application: FastAPI, + auth_client: TestClient, + project_pagination_service: ProjectPaginationService, + ) -> None: + application.dependency_overrides[get_project_pagination_service] = ( + lambda: project_pagination_service + ) + user_not_found_error = UserIdNotFoundError(1) + project_pagination_service.get_user_public_projects.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