From d6856bf7786aa64367257aac00f8e0a27daa791b Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Thu, 16 Jan 2025 21:57:04 +0100 Subject: [PATCH] fixes folders --- .../src/models_library/folders.py | 37 ++----- .../folders/_folders_repository.py | 6 +- .../folders/_folders_service.py | 5 + .../tests/unit/with_dbs/03/test_trash.py | 102 +++++++++++++++++- 4 files changed, 120 insertions(+), 30 deletions(-) diff --git a/packages/models-library/src/models_library/folders.py b/packages/models-library/src/models_library/folders.py index 0a3821fc987..24b1ed37315 100644 --- a/packages/models-library/src/models_library/folders.py +++ b/packages/models-library/src/models_library/folders.py @@ -2,14 +2,7 @@ from enum import auto from typing import TypeAlias -from pydantic import ( - BaseModel, - ConfigDict, - Field, - PositiveInt, - ValidationInfo, - field_validator, -) +from pydantic import BaseModel, ConfigDict, PositiveInt, ValidationInfo, field_validator from .access_rights import AccessRights from .groups import GroupID @@ -52,25 +45,17 @@ class FolderDB(BaseModel): folder_id: FolderID name: str parent_folder_id: FolderID | None - created_by_gid: GroupID = Field( - ..., - description="GID of the group that owns this wallet", - ) - created: datetime = Field( - ..., - description="Timestamp on creation", - ) - modified: datetime = Field( - ..., - description="Timestamp of last modification", - ) - trashed: datetime | None = Field( - ..., - ) - - user_id: UserID | None - workspace_id: WorkspaceID | None + created_by_gid: GroupID + created: datetime + modified: datetime + + trashed: datetime | None + trashed_by: UserID | None + trashed_explicitly: bool + + user_id: UserID | None # owner? + workspace_id: WorkspaceID | None model_config = ConfigDict(from_attributes=True) diff --git a/services/web/server/src/simcore_service_webserver/folders/_folders_repository.py b/services/web/server/src/simcore_service_webserver/folders/_folders_repository.py index 243fba7e858..d098573063e 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_folders_repository.py +++ b/services/web/server/src/simcore_service_webserver/folders/_folders_repository.py @@ -56,6 +56,8 @@ folders_v2.c.created, folders_v2.c.modified, folders_v2.c.trashed, + folders_v2.c.trashed_by, + folders_v2.c.trashed_explicitly, folders_v2.c.user_id, folders_v2.c.workspace_id, ) @@ -285,8 +287,8 @@ async def get( ) async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn: - result = await conn.stream(query) - row = await result.first() + result = await conn.execute(query) + row = result.first() if row is None: raise FolderAccessForbiddenError( reason=f"Folder {folder_id} does not exist.", diff --git a/services/web/server/src/simcore_service_webserver/folders/_folders_service.py b/services/web/server/src/simcore_service_webserver/folders/_folders_service.py index e9960512558..ab2ed2be990 100644 --- a/services/web/server/src/simcore_service_webserver/folders/_folders_service.py +++ b/services/web/server/src/simcore_service_webserver/folders/_folders_service.py @@ -93,6 +93,7 @@ async def create_folder( created_at=folder_db.created, modified_at=folder_db.modified, trashed_at=folder_db.trashed, + trashed_by=folder_db.trashed_by, owner=folder_db.created_by_gid, workspace_id=workspace_id, my_access_rights=user_folder_access_rights, @@ -136,6 +137,7 @@ async def get_folder( created_at=folder_db.created, modified_at=folder_db.modified, trashed_at=folder_db.trashed, + trashed_by=folder_db.trashed_by, owner=folder_db.created_by_gid, workspace_id=folder_db.workspace_id, my_access_rights=user_folder_access_rights, @@ -186,6 +188,7 @@ async def list_folders( created_at=folder.created, modified_at=folder.modified, trashed_at=folder.trashed, + trashed_by=folder.trashed_by, owner=folder.created_by_gid, workspace_id=folder.workspace_id, my_access_rights=folder.my_access_rights, @@ -230,6 +233,7 @@ async def list_folders_full_depth( created_at=folder.created, modified_at=folder.modified, trashed_at=folder.trashed, + trashed_by=folder.trashed_by, owner=folder.created_by_gid, workspace_id=folder.workspace_id, my_access_rights=folder.my_access_rights, @@ -307,6 +311,7 @@ async def update_folder( created_at=folder_db.created, modified_at=folder_db.modified, trashed_at=folder_db.trashed, + trashed_by=folder_db.trashed_by, owner=folder_db.created_by_gid, workspace_id=folder_db.workspace_id, my_access_rights=user_folder_access_rights, diff --git a/services/web/server/tests/unit/with_dbs/03/test_trash.py b/services/web/server/tests/unit/with_dbs/03/test_trash.py index 6b8173aa467..b6c10e64f2a 100644 --- a/services/web/server/tests/unit/with_dbs/03/test_trash.py +++ b/services/web/server/tests/unit/with_dbs/03/test_trash.py @@ -23,9 +23,10 @@ from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.monkeypatch_envs import setenvs_from_dict from pytest_simcore.helpers.typing_env import EnvVarsDict -from pytest_simcore.helpers.webserver_login import UserInfoDict +from pytest_simcore.helpers.webserver_login import NewUser, UserInfoDict from servicelib.aiohttp import status from simcore_service_webserver.db.models import UserRole +from simcore_service_webserver.projects._groups_api import ProjectGroupGet from simcore_service_webserver.projects.models import ProjectDict from yarl import URL @@ -74,7 +75,7 @@ async def test_trash_projects( # noqa: PLR0915 ): assert client.app - # this test should have no errors stopping services + # this test should emulate NO errors stopping services mock_remove_dynamic_services = mocker.patch( "simcore_service_webserver.projects._trash_service.projects_api.remove_project_dynamic_services", autospec=True, @@ -182,6 +183,103 @@ async def test_trash_projects( # noqa: PLR0915 mock_remove_dynamic_services.assert_awaited() +@pytest.fixture +async def other_user( + client: TestClient, logged_user: UserInfoDict +) -> AsyncIterable[UserInfoDict]: + # new user different from logged_user + async with NewUser( + { + "name": f"other_user_than_{logged_user['name']}", + "role": "USER", + }, + client.app, + ) as user: + yield user + + +async def test_trash_projects_shared_among_users( + client: TestClient, + logged_user: UserInfoDict, + user_project: ProjectDict, + other_user: UserInfoDict, + mocked_catalog: None, + mocked_dynamic_services_interface: dict[str, MagicMock], +): + assert client.app + + project_uuid = UUID(user_project["uuid"]) + + # GET project + url = client.app.router["get_project"].url_for(project_id=f"{project_uuid}") + resp = await client.get(f"{url}") + data, _ = await assert_status(resp, status.HTTP_200_OK) + + project = ProjectGet.model_validate(data) + assert project.uuid == project_uuid + assert project.prj_owner == logged_user["email"] + + # SHARE PROJECT with other-user + url = client.app.router["create_project_group"].url_for( + project_id=f"{project_uuid}", group_id=f"{other_user['primary_gid']}" + ) + resp = await client.post( + f"{url}", + json={"read": True, "write": True, "delete": False}, + ) + data, _ = await assert_status(resp, status.HTTP_201_CREATED) + + project_group = ProjectGroupGet.model_validate(data) + assert project_group.gid == other_user["primary_gid"] + assert project_group.read is True + assert project_group.write is True + assert project_group.delete is False + + # TRASH project + trashing_at = arrow.utcnow().datetime + resp = await client.post( + f"/v0/projects/{project_uuid}:trash", params={"force": "true"} + ) + await assert_status(resp, status.HTTP_204_NO_CONTENT) + + # LIST trashed of logged_user + resp = await client.get("/v0/projects", params={"filters": '{"trashed": true}'}) + await assert_status(resp, status.HTTP_200_OK) + + page = Page[ProjectListItem].model_validate(await resp.json()) + assert page.meta.total == 1 + assert page.data[0].uuid == project_uuid + assert page.data[0].trashed_at + assert trashing_at < page.data[0].trashed_at + assert page.data[0].trashed_by == logged_user["id"] + + # Swith USER: LOGOUT + url = client.app.router["auth_logout"].url_for() + resp = await client.post(f"{url}") + await assert_status(resp, status.HTTP_200_OK) + + url = client.app.router["auth_login"].url_for() + resp = await client.post( + f"{url}", + json={ + "email": other_user["email"], + "password": other_user["raw_password"], + }, + ) + data, _ = await assert_status(resp, status.HTTP_200_OK) + + # LIST trashed of another_user + resp = await client.get("/v0/projects", params={"filters": '{"trashed": true}'}) + await assert_status(resp, status.HTTP_200_OK) + + page = Page[ProjectListItem].model_validate(await resp.json()) + assert page.meta.total == 1 + assert page.data[0].uuid == project_uuid + assert page.data[0].trashed_at + assert trashing_at < page.data[0].trashed_at + assert page.data[0].trashed_by == logged_user["id"] + + @pytest.mark.acceptance_test( "For https://github.com/ITISFoundation/osparc-simcore/pull/6642" )