Beispiel #1
0
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 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
Beispiel #4
0
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()})
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),
    )
Beispiel #6
0
    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))
Beispiel #8
0
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)
Beispiel #9
0
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)
Beispiel #10
0
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
Beispiel #11
0
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,
    )
Beispiel #12
0
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