async def state_project(request: web.Request) -> web.Response: user_id = request[RQT_USERID_KEY] project_uuid = request.match_info.get("project_id") # check that project exists and queries state validated_project = await projects_api.get_project_for_user( request.app, project_uuid=project_uuid, user_id=user_id, include_templates=True, include_state=True, ) project_state = ProjectState(**validated_project["state"]) return web.json_response({"data": project_state.dict()})
async def test_get_active_project( client, logged_user, user_project, client_session_id_factory: Callable, expected, socketio_client_factory: Callable, mocked_director_v2_api, ): # login with socket using client session id client_id1 = client_session_id_factory() sio = None try: sio = await socketio_client_factory(client_id1) assert sio.sid except SocketConnectionError: if expected == web.HTTPOk: pytest.fail("socket io connection should not fail") # get active projects -> empty get_active_projects_url = ( client.app.router["get_active_project"].url_for().with_query( client_session_id=client_id1)) resp = await client.get(get_active_projects_url) data, error = await assert_status(resp, expected) if resp.status == web.HTTPOk.status_code: assert not data assert not error # open project open_project_url = client.app.router["open_project"].url_for( project_id=user_project["uuid"]) resp = await client.post(open_project_url, json=client_id1) await assert_status(resp, expected) resp = await client.get(get_active_projects_url) data, error = await assert_status(resp, expected) if resp.status == web.HTTPOk.status_code: assert not error assert ProjectState(**data.pop("state")).locked.value assert data == user_project # login with socket using client session id2 client_id2 = client_session_id_factory() try: sio = await socketio_client_factory(client_id2) assert sio.sid except SocketConnectionError: if expected == web.HTTPOk: pytest.fail("socket io connection should not fail") # get active projects -> empty get_active_projects_url = ( client.app.router["get_active_project"].url_for().with_query( client_session_id=client_id2)) resp = await client.get(get_active_projects_url) data, error = await assert_status(resp, expected) if resp.status == web.HTTPOk.status_code: assert not data assert not error
async def state_project(request: web.Request) -> web.Response: from servicelib.aiohttp.rest_utils import extract_and_validate user_id: int = request[RQT_USERID_KEY] path, _, _ = await extract_and_validate(request) project_uuid = path["project_id"] # check that project exists and queries state validated_project = await projects_api.get_project_for_user( request.app, project_uuid=project_uuid, user_id=user_id, include_templates=True, include_state=True, ) project_state = ProjectState(**validated_project["state"]) return web.json_response({"data": project_state.dict()}, dumps=json_dumps)
async def test_list_projects( client: TestClient, logged_user: Dict[str, Any], user_project: Dict[str, Any], template_project: Dict[str, Any], expected: Type[web.HTTPException], catalog_subsystem_mock: Callable[[Optional[Union[List[Dict], Dict]]], None], director_v2_service_mock: aioresponses, ): catalog_subsystem_mock([user_project, template_project]) data, *_ = await _list_projects(client, expected) if data: assert len(data) == 2 project_state = data[0].pop("state") assert data[0] == template_project assert not ProjectState( **project_state).locked.value, "Templates are not locked" project_state = data[1].pop("state") assert data[1] == user_project assert ProjectState(**project_state) # GET /v0/projects?type=user data, *_ = await _list_projects(client, expected, {"type": "user"}) if data: assert len(data) == 1 project_state = data[0].pop("state") assert data[0] == user_project assert not ProjectState( **project_state).locked.value, "Single user does not lock" # GET /v0/projects?type=template # instead /v0/projects/templates ?? data, *_ = await _list_projects(client, expected, {"type": "template"}) if data: assert len(data) == 1 project_state = data[0].pop("state") assert data[0] == template_project assert not ProjectState( **project_state).locked.value, "Templates are not locked"
async def _state_project( client, project: Dict, expected: Type[web.HTTPException], expected_project_state: ProjectState, ): url = client.app.router["state_project"].url_for(project_id=project["uuid"]) resp = await client.get(url) data, error = await assert_status(resp, expected) if not error: # the project is locked received_state = ProjectState(**data) assert received_state == expected_project_state
def mocks_on_projects_api(mocker) -> Dict: """ All projects in this module are UNLOCKED """ state = ProjectState( locked=ProjectLocked( value=False, owner=Owner(user_id=2, first_name="Speedy", last_name="Gonzalez"), ), state=ProjectRunningState(value=RunningState.NOT_STARTED), ).dict(by_alias=True, exclude_unset=True) mocker.patch( "simcore_service_webserver.projects.projects_api.get_project_state_for_user", return_value=future_with_result(state), )
async def _assert_it( client, project: Dict, expected: Type[web.HTTPException], ) -> Dict: # GET /v0/projects/{project_id} with a project owned by user url = client.app.router["get_project"].url_for(project_id=project["uuid"]) resp = await client.get(url) data, error = await assert_status(resp, expected) if not error: project_state = data.pop("state") assert data == project assert ProjectState(**project_state) return data
async def get_project_states_for_user(user_id: int, project_uuid: str, app: web.Application) -> ProjectState: # for templates: the project is never locked and never opened. also the running state is always unknown lock_state = ProjectLocked(value=False, status=ProjectStatus.CLOSED) running_state = RunningState.UNKNOWN lock_state, computation_task = await logged_gather( _get_project_lock_state(user_id, project_uuid, app), director_v2_api.get_computation_task(app, user_id, UUID(project_uuid)), ) if computation_task: # get the running state running_state = computation_task.state return ProjectState(locked=lock_state, state=ProjectRunningState(value=running_state))
async def get_project_state_for_user(user_id, project_uuid, app) -> Dict: """ Returns state of a project with respect to a given user E.g. the state is locked for user1 because user2 is working on it and there is a locked-while-using policy in place WARNING: assumes project_uuid exists!! If not, call first get_project_for_user NOTE: This adds a dependency to the socket registry sub-module. Many tests might require a mock for this function to work properly """ lock_state = await _get_project_lock_state(user_id, project_uuid, app) running_state = await _get_project_running_state(user_id, project_uuid, app) return ProjectState( locked=lock_state, state=running_state, ).dict(by_alias=True, exclude_unset=True)
async def assert_get_same_project( client: TestClient, project: ProjectDict, expected: Type[web.HTTPException], api_vtag="/v0", ) -> Dict: # GET /v0/projects/{project_id} # with a project owned by user url = client.app.router["get_project"].url_for(project_id=project["uuid"]) assert str(url) == f"{api_vtag}/projects/{project['uuid']}" resp = await client.get(url) data, error = await assert_status(resp, expected) if not error: project_state = data.pop("state") assert data == project assert ProjectState(**project_state) return data
def mocks_on_projects_api(mocker, logged_user) -> None: """ All projects in this module are UNLOCKED Emulates that it found logged_user as the SOLE user of this project and returns the ProjectState indicating his as owner """ nameparts = logged_user["name"].split(".") + [""] state = ProjectState( locked=ProjectLocked( value=False, owner=Owner( user_id=logged_user["id"], first_name=nameparts[0], last_name=nameparts[1], ), status=ProjectStatus.CLOSED, ), state=ProjectRunningState(value=RunningState.NOT_STARTED), ) mocker.patch( "simcore_service_webserver.projects.projects_api._get_project_lock_state", return_value=state, )
async def add_project_states_for_user( user_id: int, project: Dict[str, Any], is_template: bool, app: web.Application, ) -> Dict[str, Any]: # for templates: the project is never locked and never opened. also the running state is always unknown lock_state = ProjectLocked(value=False, status=ProjectStatus.CLOSED) running_state = RunningState.UNKNOWN if not is_template: lock_state, computation_task = await logged_gather( _get_project_lock_state(user_id, project["uuid"], app), director_v2_api.get_computation_task(app, user_id, project["uuid"]), ) if computation_task: # get the running state running_state = computation_task.state # get the nodes individual states for ( node_id, node_state, ) in computation_task.pipeline_details.node_states.items(): prj_node = project["workbench"].get(str(node_id)) if prj_node is None: continue node_state_dict = json.loads( node_state.json(by_alias=True, exclude_unset=True) ) prj_node.setdefault("state", {}).update(node_state_dict) project["state"] = ProjectState( locked=lock_state, state=ProjectRunningState(value=running_state) ).dict(by_alias=True, exclude_unset=True) return project
) # for templates: the project is never locked and never opened. also the running state is always unknown lock_state = ProjectLocked(value=False, status=ProjectStatus.CLOSED) running_state = RunningState.UNKNOWN if not is_template: lock_state = await _get_project_lock_state(user_id, project["uuid"], app) if computation_task := await director_v2_api.get_computation_task( app, user_id, project["uuid"]): # get the running state running_state = computation_task.state # get the nodes individual states for ( node_id, node_state, ) in computation_task.pipeline_details.node_states.items(): prj_node = project["workbench"].get(str(node_id)) if prj_node is None: continue node_state_dict = json.loads( node_state.json(by_alias=True, exclude_unset=True)) prj_node.setdefault("state", {}).update(node_state_dict) project["state"] = ProjectState( locked=lock_state, state=ProjectRunningState(value=running_state)).dict( by_alias=True, exclude_unset=True) return project
async def test_open_shared_project_2_users_locked( client: TestClient, client_on_running_server_factory: Callable, logged_user: Dict, shared_project: Dict, socketio_client_factory: Callable, client_session_id_factory: Callable, user_role: UserRole, expected: ExpectedResponse, mocker, disable_gc_manual_guest_users, ): # Use-case: user 1 opens a shared project, user 2 tries to open it as well mock_project_state_updated_handler = mocker.Mock() client_1 = client client_id1 = client_session_id_factory() client_2 = client_on_running_server_factory() client_id2 = client_session_id_factory() # 1. user 1 opens project sio_1 = await _connect_websocket( socketio_client_factory, user_role != UserRole.ANONYMOUS, client_1, client_id1, {SOCKET_IO_PROJECT_UPDATED_EVENT: mock_project_state_updated_handler}, ) # expected is that the project is closed and unlocked expected_project_state_client_1 = ProjectState( locked=ProjectLocked(value=False, status=ProjectStatus.CLOSED), state=ProjectRunningState(value=RunningState.NOT_STARTED), ) for client_id in [client_id1, None]: await _state_project( client_1, shared_project, expected.ok if user_role != UserRole.GUEST else web.HTTPOk, expected_project_state_client_1, ) await _open_project( client_1, client_id1, shared_project, expected.ok if user_role != UserRole.GUEST else web.HTTPOk, ) # now the expected result is that the project is locked and opened by client 1 owner1 = Owner( user_id=logged_user["id"], first_name=(logged_user["name"].split(".") + [""])[0], last_name=(logged_user["name"].split(".") + [""])[1], ) expected_project_state_client_1.locked.value = True expected_project_state_client_1.locked.status = ProjectStatus.OPENED expected_project_state_client_1.locked.owner = owner1 # NOTE: there are 2 calls since we are part of the primary group and the all group await _assert_project_state_updated( mock_project_state_updated_handler, shared_project, [expected_project_state_client_1] * (0 if user_role == UserRole.ANONYMOUS else 2), ) await _state_project( client_1, shared_project, expected.ok if user_role != UserRole.GUEST else web.HTTPOk, expected_project_state_client_1, ) # 2. create a separate client now and log in user2, try to open the same shared project user_2 = await log_client_in( client_2, {"role": user_role.name}, enable_check=user_role != UserRole.ANONYMOUS ) sio_2 = await _connect_websocket( socketio_client_factory, user_role != UserRole.ANONYMOUS, client_2, client_id2, {SOCKET_IO_PROJECT_UPDATED_EVENT: mock_project_state_updated_handler}, ) await _open_project( client_2, client_id2, shared_project, expected.locked if user_role != UserRole.GUEST else HTTPLocked, ) expected_project_state_client_2 = deepcopy(expected_project_state_client_1) expected_project_state_client_2.locked.status = ProjectStatus.OPENED await _state_project( client_2, shared_project, expected.ok if user_role != UserRole.GUEST else web.HTTPOk, expected_project_state_client_2, ) # 3. user 1 closes the project await _close_project(client_1, client_id1, shared_project, expected.no_content) if not any(user_role == role for role in [UserRole.ANONYMOUS, UserRole.GUEST]): # Guests cannot close projects expected_project_state_client_1 = ProjectState( locked=ProjectLocked(value=False, status=ProjectStatus.CLOSED), state=ProjectRunningState(value=RunningState.NOT_STARTED), ) # we should receive an event that the project lock state changed # NOTE: there are 2x3 calls since we are part of the primary group and the all group and user 2 is part of the all group # first CLOSING, then CLOSED await _assert_project_state_updated( mock_project_state_updated_handler, shared_project, [ expected_project_state_client_1.copy( update={ "locked": ProjectLocked( value=True, status=ProjectStatus.CLOSING, owner=owner1 ) } ) ] * ( 0 if any(user_role == role for role in [UserRole.ANONYMOUS, UserRole.GUEST]) else 3 ) + [expected_project_state_client_1] * ( 0 if any(user_role == role for role in [UserRole.ANONYMOUS, UserRole.GUEST]) else 3 ), ) await _state_project( client_1, shared_project, expected.ok if user_role != UserRole.GUEST else web.HTTPOk, expected_project_state_client_1, ) # 4. user 2 now should be able to open the project await _open_project( client_2, client_id2, shared_project, expected.ok if user_role != UserRole.GUEST else HTTPLocked, ) if not any(user_role == role for role in [UserRole.ANONYMOUS, UserRole.GUEST]): expected_project_state_client_2.locked.value = True expected_project_state_client_2.locked.status = ProjectStatus.OPENED owner2 = Owner( user_id=user_2["id"], first_name=(user_2["name"].split(".") + [""])[0], last_name=(user_2["name"].split(".") + [""])[1], ) expected_project_state_client_2.locked.owner = owner2 expected_project_state_client_1.locked.value = True expected_project_state_client_1.locked.status = ProjectStatus.OPENED expected_project_state_client_1.locked.owner = owner2 # NOTE: there are 3 calls since we are part of the primary group and the all group await _assert_project_state_updated( mock_project_state_updated_handler, shared_project, [expected_project_state_client_1] * ( 0 if any(user_role == role for role in [UserRole.ANONYMOUS, UserRole.GUEST]) else 3 ), ) await _state_project( client_1, shared_project, expected.ok if user_role != UserRole.GUEST else web.HTTPOk, expected_project_state_client_1, )
async def _new_project( client, expected_response: Type[web.HTTPException], logged_user: Dict[str, str], primary_group: Dict[str, str], *, project: Optional[Dict] = None, from_template: Optional[Dict] = None, ) -> Dict: # POST /v0/projects url = client.app.router["create_projects"].url_for() assert str(url) == f"{API_PREFIX}/projects" if from_template: url = url.with_query(from_template=from_template["uuid"]) # Pre-defined fields imposed by required properties in schema project_data = {} expected_data = {} if from_template: # access rights are replaced expected_data = deepcopy(from_template) expected_data["accessRights"] = {} if not from_template or project: project_data = { "uuid": "0000000-invalid-uuid", "name": "Minimal name", "description": "this description should not change", "prjOwner": "me but I will be removed anyway", "creationDate": now_str(), "lastChangeDate": now_str(), "thumbnail": "", "accessRights": {}, "workbench": {}, "tags": [], "classifiers": [], "ui": {}, "dev": {}, "quality": {}, } if project: project_data.update(project) for key in project_data: expected_data[key] = project_data[key] if ( key in OVERRIDABLE_DOCUMENT_KEYS and not project_data[key] and from_template ): expected_data[key] = from_template[key] resp = await client.post(url, json=project_data) new_project, error = await assert_status(resp, expected_response) if not error: # has project state assert not ProjectState( **new_project.pop("state") ).locked.value, "Newly created projects should be unlocked" # updated fields assert expected_data["uuid"] != new_project["uuid"] assert ( new_project["prjOwner"] == logged_user["email"] ) # the project owner is assigned the user id e-mail assert to_datetime(expected_data["creationDate"]) < to_datetime( new_project["creationDate"] ) assert to_datetime(expected_data["lastChangeDate"]) < to_datetime( new_project["lastChangeDate"] ) # the access rights are set to use the logged user primary group + whatever was inside the project expected_data["accessRights"].update( {str(primary_group["gid"]): {"read": True, "write": True, "delete": True}} ) assert new_project["accessRights"] == expected_data["accessRights"] # invariant fields modified_fields = [ "uuid", "prjOwner", "creationDate", "lastChangeDate", "accessRights", "workbench" if from_template else None, "ui" if from_template else None, ] for key in new_project.keys(): if key not in modified_fields: assert expected_data[key] == new_project[key] return new_project
async def test_open_shared_project_at_same_time( loop, client: TestClient, client_on_running_server_factory: Callable, logged_user: Dict, shared_project: Dict, socketio_client_factory: Callable, client_session_id_factory: Callable, user_role: UserRole, expected: ExpectedResponse, disable_gc_manual_guest_users, ): NUMBER_OF_ADDITIONAL_CLIENTS = 20 # log client 1 client_1 = client client_id1 = client_session_id_factory() sio_1 = await _connect_websocket( socketio_client_factory, user_role != UserRole.ANONYMOUS, client_1, client_id1, ) clients = [ {"client": client_1, "user": logged_user, "client_id": client_id1, "sio": sio_1} ] # create other clients for i in range(NUMBER_OF_ADDITIONAL_CLIENTS): new_client = client_on_running_server_factory() user = await log_client_in( new_client, {"role": user_role.name}, enable_check=user_role != UserRole.ANONYMOUS, ) client_id = client_session_id_factory() sio = await _connect_websocket( socketio_client_factory, user_role != UserRole.ANONYMOUS, new_client, client_id, ) clients.append( {"client": new_client, "user": user, "client_id": client_id, "sio": sio} ) # try opening projects at same time (more or less) open_project_tasks = [ _open_project( c["client"], c["client_id"], shared_project, [ expected.ok if user_role != UserRole.GUEST else web.HTTPOk, expected.locked if user_role != UserRole.GUEST else HTTPLocked, ], ) for c in clients ] results = await asyncio.gather( *open_project_tasks, return_exceptions=True, ) # one should be opened, the other locked if user_role != UserRole.ANONYMOUS: num_assertions = 0 for data, error in results: assert data or error if error: num_assertions += 1 elif data: project_status = ProjectState(**data.pop("state")) assert data == shared_project assert project_status.locked.value assert project_status.locked.owner.first_name in [ c["user"]["name"] for c in clients ] assert num_assertions == NUMBER_OF_ADDITIONAL_CLIENTS
async def test_workflow( postgres_db: sa.engine.Engine, docker_registry: str, simcore_services_ready, fake_project: ProjectDict, catalog_subsystem_mock, client, logged_user, primary_group: Dict[str, str], standard_groups: List[Dict[str, str]], storage_subsystem_mock, director_v2_service_mock, ): # empty list projects = await _request_list(client) assert not projects # creation await _request_create(client, fake_project) catalog_subsystem_mock([fake_project]) # list not empty projects = await _request_list(client) assert len(projects) == 1 assert not ProjectState(**projects[0].pop("state")).locked.value for key in projects[0].keys(): if key not in ( "uuid", "prjOwner", "creationDate", "lastChangeDate", "accessRights", ): assert projects[0][key] == fake_project[key] assert projects[0]["prjOwner"] == logged_user["email"] assert projects[0]["accessRights"] == { str(primary_group["gid"]): {"read": True, "write": True, "delete": True} } modified_project = deepcopy(projects[0]) modified_project["name"] = "some other name" modified_project["description"] = "John Raynor killed Kerrigan" new_node_id = str(uuid4()) modified_project["workbench"][new_node_id] = modified_project["workbench"].pop( list(modified_project["workbench"].keys())[0] ) modified_project["workbench"][new_node_id]["position"]["x"] = 0 # share with some group modified_project["accessRights"].update( {str(standard_groups[0]["gid"]): {"read": True, "write": True, "delete": False}} ) # modify pid = modified_project["uuid"] await _request_update(client, modified_project, pid) # list not empty projects = await _request_list(client) assert len(projects) == 1 for key in projects[0].keys(): if key not in ("lastChangeDate", "state"): assert projects[0][key] == modified_project[key] # get project = await _request_get(client, pid) for key in project.keys(): if key not in ("lastChangeDate", "state"): assert project[key] == modified_project[key] # delete await _request_delete(client, pid) # wait for delete tasks to finish tasks = asyncio.all_tasks() for task in tasks: # TODO: 'async_generator_asend' has no __name__ attr. Python 3.8 gets coros names # Expects "delete_project" coros to have __name__ attrs # pylint: disable=protected-access if "delete_project" in getattr(task.get_coro(), "__name__", ""): await asyncio.wait_for(task, timeout=60.0) # list empty projects = await _request_list(client) assert not projects