diff --git a/packages/models-library/src/models_library/api_schemas_webserver/projects.py b/packages/models-library/src/models_library/api_schemas_webserver/projects.py index 2d8cd69ab93..601ac1e6d15 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/projects.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/projects.py @@ -121,18 +121,6 @@ class ProjectReplace(InputSchema): ) -class ProjectUpdate(InputSchema): - name: ShortTruncatedStr = FieldNotRequired() - description: LongTruncatedStr = FieldNotRequired() - thumbnail: HttpUrlWithCustomMinLength = FieldNotRequired() - workbench: NodesDict = FieldNotRequired() - access_rights: dict[GroupIDStr, AccessRights] = FieldNotRequired() - tags: list[int] = FieldNotRequired() - classifiers: list[ClassifierID] = FieldNotRequired() - ui: StudyUI | None = None - quality: dict[str, Any] = FieldNotRequired() - - class ProjectPatch(InputSchema): name: ShortTruncatedStr = FieldNotRequired() description: LongTruncatedStr = FieldNotRequired() @@ -143,6 +131,10 @@ class ProjectPatch(InputSchema): ui: StudyUI | None = FieldNotRequired() quality: dict[str, Any] = FieldNotRequired() + _empty_is_none = validator("thumbnail", allow_reuse=True, pre=True)( + empty_str_to_none_pre_validator + ) + __all__: tuple[str, ...] = ( "EmptyModel", @@ -151,6 +143,5 @@ class ProjectPatch(InputSchema): "ProjectGet", "ProjectListItem", "ProjectReplace", - "ProjectUpdate", "TaskProjectGet", ) diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py index 9b0a1f53562..9111fda161a 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_handlers.py @@ -101,7 +101,7 @@ async def _wrapper(request: web.Request) -> web.StreamResponse: FolderAccessForbiddenError, WorkspaceAccessForbiddenError, ) as exc: - raise web.HTTPUnauthorized(reason=f"{exc}") from exc + raise web.HTTPForbidden(reason=f"{exc}") from exc return _wrapper diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers.py index ec856a80f05..cc5b59af42e 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_crud_handlers.py @@ -4,11 +4,9 @@ # pylint: disable=unused-argument # pylint: disable=unused-variable -import random import re import uuid as uuidlib from collections.abc import Awaitable, Callable, Iterator -from copy import deepcopy from http import HTTPStatus from math import ceil from typing import Any @@ -19,10 +17,7 @@ from aioresponses import aioresponses from faker import Faker from models_library.products import ProductName -from models_library.projects_nodes import Node from models_library.projects_state import ProjectState -from models_library.services import ServiceKey -from models_library.utils.fastapi_encoders import jsonable_encoder from pydantic import parse_obj_as from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.webserver_login import UserInfoDict @@ -187,7 +182,7 @@ async def _replace_project( assert client.app # PUT /v0/projects/{project_id} - url = client.app.router["replace_project"].url_for( + url = client.app.router["replace_project"].url_for( # <- MD: check this project_id=project_update["uuid"] ) assert str(url) == f"{API_PREFIX}/projects/{project_update['uuid']}" @@ -656,141 +651,6 @@ async def test_new_template_from_project( parse_obj_as(uuidlib.UUID, node_name) -# PUT -------- -@pytest.mark.parametrize( - "user_role,expected,expected_change_access", - [ - ( - UserRole.ANONYMOUS, - status.HTTP_401_UNAUTHORIZED, - status.HTTP_401_UNAUTHORIZED, - ), - (UserRole.GUEST, status.HTTP_200_OK, status.HTTP_403_FORBIDDEN), - (UserRole.USER, status.HTTP_200_OK, status.HTTP_200_OK), - (UserRole.TESTER, status.HTTP_200_OK, status.HTTP_200_OK), - ], -) -async def test_replace_project( - client: TestClient, - logged_user: UserInfoDict, - user_project: ProjectDict, - expected, - expected_change_access, - all_group, - ensure_run_in_sequence_context_is_empty, -): - project_update = deepcopy(user_project) - project_update["description"] = "some updated from original project!!!" - await _replace_project(client, project_update, expected) - - # replacing the owner access is not possible, it will keep the owner as well - project_update["accessRights"].update( - {str(all_group["gid"]): {"read": True, "write": True, "delete": True}} - ) - await _replace_project(client, project_update, expected_change_access) - - -@pytest.mark.parametrize( - "user_role,expected", - [ - (UserRole.ANONYMOUS, status.HTTP_401_UNAUTHORIZED), - (UserRole.GUEST, status.HTTP_200_OK), - (UserRole.USER, status.HTTP_200_OK), - (UserRole.TESTER, status.HTTP_200_OK), - ], -) -async def test_replace_project_updated_inputs( - client: TestClient, - logged_user: UserInfoDict, - user_project: ProjectDict, - expected, - ensure_run_in_sequence_context_is_empty, -): - project_update = deepcopy(user_project) - # - # "inputAccess": { - # "Na": "ReadAndWrite", <-------- - # "Kr": "ReadOnly", - # "BCL": "ReadAndWrite", - # "NBeats": "ReadOnly", - # "Ligand": "Invisible", - # "cAMKII": "Invisible" - # }, - project_update["workbench"]["5739e377-17f7-4f09-a6ad-62659fb7fdec"]["inputs"][ - "Na" - ] = 55 - await _replace_project(client, project_update, expected) - - -@pytest.mark.parametrize( - "user_role,expected", - [ - (UserRole.ANONYMOUS, status.HTTP_401_UNAUTHORIZED), - (UserRole.GUEST, status.HTTP_200_OK), - (UserRole.USER, status.HTTP_200_OK), - (UserRole.TESTER, status.HTTP_200_OK), - ], -) -async def test_replace_project_updated_readonly_inputs( - client: TestClient, - logged_user: UserInfoDict, - user_project: ProjectDict, - expected, - ensure_run_in_sequence_context_is_empty, -): - project_update = deepcopy(user_project) - project_update["workbench"]["5739e377-17f7-4f09-a6ad-62659fb7fdec"]["inputs"][ - "Na" - ] = 55 - project_update["workbench"]["5739e377-17f7-4f09-a6ad-62659fb7fdec"]["inputs"][ - "Kr" - ] = 5 - await _replace_project(client, project_update, expected) - - -@pytest.fixture -def random_minimal_node(faker: Faker) -> Callable[[], Node]: - def _creator() -> Node: - return Node( - key=ServiceKey(f"simcore/services/comp/{faker.pystr().lower()}"), - version=faker.numerify("#.#.#"), - label=faker.pystr(), - ) - - return _creator - - -@pytest.mark.parametrize( - "user_role,expected", - [ - (UserRole.ANONYMOUS, status.HTTP_401_UNAUTHORIZED), - (UserRole.GUEST, status.HTTP_409_CONFLICT), - (UserRole.USER, status.HTTP_409_CONFLICT), - (UserRole.TESTER, status.HTTP_409_CONFLICT), - ], -) -async def test_replace_project_adding_or_removing_nodes_raises_conflict( - client: TestClient, - logged_user: UserInfoDict, - user_project: ProjectDict, - expected, - ensure_run_in_sequence_context_is_empty, - faker: Faker, - random_minimal_node: Callable[[], Node], -): - # try adding a node should not work - project_update = deepcopy(user_project) - new_node = random_minimal_node() - project_update["workbench"][faker.uuid4()] = jsonable_encoder(new_node) - await _replace_project(client, project_update, expected) - # try removing a node should not work - project_update = deepcopy(user_project) - project_update["workbench"].pop( - random.choice(list(project_update["workbench"].keys())) # noqa: S311 - ) - await _replace_project(client, project_update, expected) - - @pytest.fixture def mock_director_v2_inactivity( aioresponses_mocker: aioresponses, is_inactive: bool diff --git a/services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py b/services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py index d81dbbdb352..02285ebb0d5 100644 --- a/services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py +++ b/services/web/server/tests/unit/with_dbs/02/test_projects_states_handlers.py @@ -112,15 +112,18 @@ async def _replace_project( ) -> ProjectDict: assert client.app - # PUT /v0/projects/{project_id} - url = client.app.router["replace_project"].url_for( - project_id=project_update["uuid"] - ) + # PATCH /v0/projects/{project_id} + url = client.app.router["patch_project"].url_for(project_id=project_update["uuid"]) assert str(url) == f"{API_PREFIX}/projects/{project_update['uuid']}" - resp = await client.put(f"{url}", json=project_update) + resp = await client.patch(f"{url}", json=project_update) data, error = await assert_status(resp, expected) if not error: - assert_replaced(current_project=data, update_data=project_update) + url = client.app.router["get_project"].url_for( + project_id=project_update["uuid"] + ) + resp = await client.get(f"{url}") + get_data, _ = await assert_status(resp, HTTPStatus.OK) + assert_replaced(current_project=get_data, update_data=project_update) return data @@ -307,10 +310,11 @@ async def test_share_project( # user 2 can update the project if user 2 has write access project_update = deepcopy(new_project) project_update["name"] = "my super name" + project_update.pop("accessRights") await _replace_project( client, project_update, - expected.ok if share_rights["write"] else expected.forbidden, + expected.no_content if share_rights["write"] else expected.forbidden, ) # user 2 can delete projects if user 2 has delete access diff --git a/services/web/server/tests/unit/with_dbs/03/meta_modeling/test_meta_modeling_iterations.py b/services/web/server/tests/unit/with_dbs/03/meta_modeling/test_meta_modeling_iterations.py index 20cb885bdfa..61fa226a280 100644 --- a/services/web/server/tests/unit/with_dbs/03/meta_modeling/test_meta_modeling_iterations.py +++ b/services/web/server/tests/unit/with_dbs/03/meta_modeling/test_meta_modeling_iterations.py @@ -11,7 +11,7 @@ from models_library.projects import Project from models_library.projects_nodes import Node from models_library.services_resources import ServiceResourcesDict -from models_library.utils.json_serialization import json_dumps +from models_library.utils.json_serialization import json_dumps, json_loads from pytest_mock import MockerFixture from pytest_simcore.helpers.assert_checks import assert_status from pytest_simcore.helpers.monkeypatch_envs import EnvVarsDict, setenvs_from_dict @@ -34,6 +34,7 @@ meta_project_policy, projects_redirection_middleware, ) +from simcore_service_webserver.projects.db import ProjectDBAPI from simcore_service_webserver.projects.models import ProjectDict REQUEST_MODEL_POLICY = { @@ -144,13 +145,22 @@ async def test_iterators_workflow( project_data.update({key: modifications[key] for key in ("workbench", "ui")}) project_data["ui"].setdefault("currentNodeId", project_uuid) - response = await client.put( - f"/v0/projects/{project_data['uuid']}", - json=project_data, + # response = await client.patch( + # f"/v0/projects/{project_data['uuid']}", + # json=project_data, + # ) + # assert ( + # response.status == status.HTTP_204_NO_CONTENT + # ), await response.text() + + db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(client.app) + project_data.pop("state") + await db.replace_project( + project_data, + logged_user["id"], + project_uuid=project_uuid, + product_name="osparc", ) - assert ( - response.status == REPLACE_PROJECT_ON_MODIFIED.status_code - ), await response.text() # TODO: create iterations, so user could explore parametrizations? @@ -260,11 +270,20 @@ async def _mock_catalog_get(*args, **kwarg): assert node.inputs node.inputs["linspace_stop"] = 4 - response = await client.put( - f"/v0/projects/{project_uuid}", - data=json_dumps(new_project.dict(**REQUEST_MODEL_POLICY)), + # response = await client.patch( + # f"/v0/projects/{project_uuid}", + # data=json_dumps(new_project.dict(**REQUEST_MODEL_POLICY)), + # ) + # assert response.status == status.HTTP_204_NO_CONTENT, await response.text() + # db: ProjectDBAPI = ProjectDBAPI.get_from_app_context(client.app) + _new_project_data = new_project.dict(**REQUEST_MODEL_POLICY) + _new_project_data.pop("state") + await db.replace_project( + json_loads(json_dumps(_new_project_data)), + logged_user["id"], + project_uuid=project_uuid, + product_name="osparc", ) - assert response.status == status.HTTP_200_OK, await response.text() # RUN again them --------------------------------------------------------------------------- response = await client.post(