Beispiel #1
0
async def test_create_cluster(
    enable_dev_features: None,
    client: TestClient,
    postgres_db: sa.engine.Engine,
    logged_user: Dict[str, Any],
    faker: Faker,
    user_role: UserRole,
    cluster_authentication: Callable[[], Dict[str, Any]],
    expected: ExpectedResponse,
):
    # check we can create a cluster
    assert client.app
    url = client.app.router["create_cluster_handler"].url_for()
    cluster_data = json.loads(
        ClusterCreate(
            endpoint=faker.uri(),
            authentication=cluster_authentication(),
            name=faker.name(),
            type=random.choice(list(ClusterType)),
        ).json(by_alias=True, exclude_unset=True)
    )
    rsp = await client.post(f"{url}", json=cluster_data)
    data, error = await assert_status(
        rsp,
        expected.forbidden
        if user_role == UserRole.USER
        else expected.created,  # only accessible for TESTER
    )
    if error:
        # we are done here
        return

    created_cluster = Cluster.parse_obj(data)
    assert created_cluster

    # check database entry was correctly created
    result: ResultProxy = postgres_db.execute(
        sa.select([clusters]).where(clusters.c.name == cluster_data["name"])
    )
    assert result, "could not find cluster in database"
    row = result.fetchone()
    assert row, "could not find cluster in database"
    assert row[clusters.c.name] == cluster_data["name"]
    assert row[clusters.c.owner] == logged_user["primary_gid"]
    assert (
        Cluster(
            id=row[clusters.c.id],
            name=cluster_data["name"],
            type=row[clusters.c.type],
            endpoint=row[clusters.c.endpoint],
            authentication=row[clusters.c.authentication],
            owner=logged_user["primary_gid"],
            access_rights={logged_user["primary_gid"]: CLUSTER_ADMIN_RIGHTS},
        )
        == created_cluster
    )

    # cleanup
    postgres_db.execute(clusters.delete().where(clusters.c.id == row[clusters.c.id]))
Beispiel #2
0
def mocked_director_v2_api(mocker: MockerFixture):
    mocked_director_v2_api = mocker.patch(
        "simcore_service_webserver.clusters.handlers.director_v2_api",
        autospec=True)

    mocked_director_v2_api.create_cluster.return_value = Cluster.parse_obj(
        random.choice(Cluster.Config.schema_extra["examples"]))
    mocked_director_v2_api.list_clusters.return_value = []
    mocked_director_v2_api.get_cluster.return_value = Cluster.parse_obj(
        random.choice(Cluster.Config.schema_extra["examples"]))
    mocked_director_v2_api.get_cluster_details.return_value = {}
    mocked_director_v2_api.update_cluster.return_value = Cluster.parse_obj(
        random.choice(Cluster.Config.schema_extra["examples"]))
    mocked_director_v2_api.delete_cluster.return_value = None
    mocked_director_v2_api.ping_cluster.return_value = None
Beispiel #3
0
    def creator(user: Dict[str, Any], **cluster_kwargs) -> Cluster:
        cluster_config = Cluster.Config.schema_extra["examples"][0]
        cluster_config["owner"] = user["primary_gid"]
        cluster_config.update(**cluster_kwargs)
        new_cluster = Cluster.parse_obj(cluster_config)
        assert new_cluster

        with postgres_db.connect() as conn:
            # insert basic cluster
            created_cluster = conn.execute(
                sa.insert(clusters).values(
                    new_cluster.to_clusters_db(only_update=False)).returning(
                        sa.literal_column("*"))).one()
            created_cluster_ids.append(created_cluster.id)
            if "access_rights" in cluster_kwargs:
                for gid, rights in cluster_kwargs["access_rights"].items():
                    conn.execute(
                        pg_insert(cluster_to_groups).values(
                            cluster_id=created_cluster.id,
                            gid=gid,
                            **rights.dict()).on_conflict_do_update(
                                index_elements=["gid", "cluster_id"],
                                set_=rights.dict()))
            access_rights_in_db = {}
            for row in conn.execute(
                    sa.select([
                        cluster_to_groups.c.gid,
                        cluster_to_groups.c.read,
                        cluster_to_groups.c.write,
                        cluster_to_groups.c.delete,
                    ]).select_from(clusters.join(cluster_to_groups)).where(
                        clusters.c.id == created_cluster.id)):
                access_rights_in_db[row.gid] = {
                    "read": row[cluster_to_groups.c.read],
                    "write": row[cluster_to_groups.c.write],
                    "delete": row[cluster_to_groups.c.delete],
                }

            return Cluster.construct(
                id=created_cluster.id,
                name=created_cluster.name,
                description=created_cluster.description,
                type=created_cluster.type,
                owner=created_cluster.owner,
                endpoint=created_cluster.endpoint,
                authentication=created_cluster.authentication,
                access_rights=access_rights_in_db,
            )
Beispiel #4
0
async def test_create_cluster(
    enable_dev_features: None,
    mocked_director_v2_api,
    client: TestClient,
    logged_user: Dict[str, Any],
    faker: Faker,
    cluster_create: ClusterCreate,
    user_role: UserRole,
    expected: ExpectedResponse,
):
    cluster_create.access_rights[logged_user["id"]] = CLUSTER_ADMIN_RIGHTS
    print(f"--> creating {cluster_create=!r}")
    # check we can create a cluster
    assert client.app
    url = client.app.router["create_cluster_handler"].url_for()
    rsp = await client.post(
        f"{url}",
        json=json.loads(cluster_create.json(by_alias=True,
                                            exclude_unset=True)),
    )
    data, error = await assert_status(
        rsp,
        expected.forbidden if user_role == UserRole.USER else
        expected.created,  # only accessible for TESTER
    )
    if error:
        # we are done here
        return

    created_cluster = Cluster.parse_obj(data)
    assert created_cluster
Beispiel #5
0
 def creator(num_clusters: int) -> List[Cluster]:
     fake_clusters = []
     for n in range(num_clusters):
         fake_clusters.append(
             Cluster.parse_obj({
                 "id":
                 faker.pyint(),
                 "name":
                 faker.name(),
                 "type":
                 ClusterType.ON_PREMISE,
                 "owner":
                 faker.pyint(),
                 "endpoint":
                 faker.uri(),
                 "authentication":
                 choice([
                     NoAuthentication(),
                     SimpleAuthentication(
                         username=faker.user_name(),
                         password=faker.password(),
                     ),
                     KerberosAuthentication(),
                     JupyterHubTokenAuthentication(api_token=faker.uuid4()),
                 ]),
             }))
     return fake_clusters
Beispiel #6
0
    def creator(**overrides) -> Cluster:
        cluster_config = Cluster.Config.schema_extra["examples"][0]
        cluster_config["owner"] = user_db["primary_gid"]
        cluster_config.update(**overrides)
        new_cluster = Cluster.parse_obj(cluster_config)
        assert new_cluster

        with postgres_db.connect() as conn:
            created_cluser_id = conn.scalar(
                # pylint: disable=no-value-for-parameter
                clusters.insert().values(
                    new_cluster.to_clusters_db(only_update=False)
                ).returning(clusters.c.id))
            created_cluster_ids.append(created_cluser_id)
            result = conn.execute(
                sa.select([
                    clusters,
                    cluster_to_groups.c.gid,
                    cluster_to_groups.c.read,
                    cluster_to_groups.c.write,
                    cluster_to_groups.c.delete,
                ]).select_from(
                    clusters.join(
                        cluster_to_groups,
                        clusters.c.id == cluster_to_groups.c.cluster_id,
                    )).where(clusters.c.id == created_cluser_id))

            row = result.fetchone()
            assert row
            return Cluster.construct(
                id=row[clusters.c.id],
                name=row[clusters.c.name],
                description=row[clusters.c.description],
                type=row[clusters.c.type],
                owner=row[clusters.c.owner],
                endpoint=row[clusters.c.endpoint],
                authentication=row[clusters.c.authentication],
                access_rights={
                    row[clusters.c.owner]: {
                        "read": row[cluster_to_groups.c.read],
                        "write": row[cluster_to_groups.c.write],
                        "delete": row[cluster_to_groups.c.delete],
                    }
                },
            )
Beispiel #7
0
async def _clusters_from_cluster_ids(
    conn: sa.engine.Connection,
    cluster_ids: Set[PositiveInt],
    offset: int = 0,
    limit: Optional[int] = None,
) -> List[Cluster]:
    cluster_id_to_cluster: Dict[PositiveInt, Cluster] = {}
    async for row in conn.execute(
        sa.select(
            [
                clusters,
                cluster_to_groups.c.gid,
                cluster_to_groups.c.read,
                cluster_to_groups.c.write,
                cluster_to_groups.c.delete,
            ]
        )
        .select_from(
            clusters.join(
                cluster_to_groups,
                clusters.c.id == cluster_to_groups.c.cluster_id,
            )
        )
        .where(clusters.c.id.in_(cluster_ids))
        .offset(offset)
        .limit(limit)
    ):
        cluster_access_rights = {
            row[cluster_to_groups.c.gid]: ClusterAccessRights(
                **{
                    "read": row[cluster_to_groups.c.read],
                    "write": row[cluster_to_groups.c.write],
                    "delete": row[cluster_to_groups.c.delete],
                }
            )
        }

        cluster_id = row[clusters.c.id]
        if cluster_id not in cluster_id_to_cluster:
            cluster_id_to_cluster[cluster_id] = Cluster(
                id=cluster_id,
                name=row[clusters.c.name],
                description=row[clusters.c.description],
                type=row[clusters.c.type],
                owner=row[clusters.c.owner],
                endpoint=row[clusters.c.endpoint],
                authentication=row[clusters.c.authentication],
                thumbnail=row[clusters.c.thumbnail],
                access_rights=cluster_access_rights,
            )
        else:
            cluster_id_to_cluster[cluster_id].access_rights.update(
                cluster_access_rights
            )

    return list(cluster_id_to_cluster.values())
 def default_cluster(settings: DaskSchedulerSettings):
     return Cluster(
         id=settings.DASK_DEFAULT_CLUSTER_ID,
         name="Default internal cluster",
         type=ClusterType.ON_PREMISE,
         endpoint=
         f"tcp://{settings.DASK_SCHEDULER_HOST}:{settings.DASK_SCHEDULER_PORT}",
         authentication=NoAuthentication(),
         owner=
         1,  # FIXME: that is usually the everyone's group... but we do not know nor care about it in director-v2...
     )  # type: ignore
Beispiel #9
0
    async def creator(
        gid: GroupID, cluster_access_rights: Dict[GroupID, ClusterAccessRights] = None
    ) -> Cluster:
        new_cluster = ClusterCreate(
            **{
                "name": faker.name(),
                "type": random.choice(list(ClusterType)),
                "owner": gid,
                "access_rights": cluster_access_rights or {},
                "endpoint": faker.uri(),
                "authentication": cluster_authentication(),
            }
        )

        result = postgres_db.execute(
            clusters.insert()
            .values(new_cluster.to_clusters_db(only_update=False))
            .returning(literal_column("*"))
        )
        cluster_in_db = result.first()
        assert cluster_in_db is not None
        new_cluster_id = cluster_in_db[clusters.c.id]
        list_of_created_cluster_ids.append(new_cluster_id)

        # when a cluster is created, the DB automatically creates the owner access rights
        for group_id, access_rights in new_cluster.access_rights.items():
            result = postgres_db.execute(
                insert(cluster_to_groups)
                .values(
                    **{
                        "cluster_id": new_cluster_id,
                        "gid": group_id,
                        "read": access_rights.read,
                        "write": access_rights.write,
                        "delete": access_rights.delete,
                    }
                )
                .on_conflict_do_nothing()
            )

        return Cluster(
            id=new_cluster_id, **new_cluster.dict(by_alias=True, exclude={"id"})
        )
Beispiel #10
0
    async def create_cluster(self, new_cluster: ClusterCreate) -> Cluster:
        async with self.engine.acquire() as conn:
            created_cluser_id: int = await conn.scalar(
                # pylint: disable=no-value-for-parameter
                clusters.insert().values(
                    new_cluster.to_clusters_db(only_update=False)
                ).returning(clusters.c.id))

            result = await conn.execute(
                sa.select([
                    clusters,
                    cluster_to_groups.c.gid,
                    cluster_to_groups.c.read,
                    cluster_to_groups.c.write,
                    cluster_to_groups.c.delete,
                ]).select_from(
                    clusters.join(
                        cluster_to_groups,
                        clusters.c.id == cluster_to_groups.c.cluster_id,
                    )).where(clusters.c.id == created_cluser_id))

            row = await result.fetchone()

            assert row  # nosec

            return Cluster.construct(
                id=row[clusters.c.id],
                name=row[clusters.c.name],
                description=row[clusters.c.description],
                type=row[clusters.c.type],
                owner=row[clusters.c.owner],
                endpoint=row[clusters.c.endpoint],
                authentication=row[clusters.c.authentication],
                access_rights={
                    row[clusters.c.owner]: {
                        "read": row[cluster_to_groups.c.read],
                        "write": row[cluster_to_groups.c.write],
                        "delete": row[cluster_to_groups.c.delete],
                    }
                },
            )
Beispiel #11
0
async def test_update_cluster(
    enable_dev_features: None,
    client: TestClient,
    logged_user: Dict[str, Any],
    second_user: Dict[str, Any],
    all_group: Dict[str, Any],
    cluster: Callable[..., Coroutine[Any, Any, Cluster]],
    user_role: UserRole,
    expected: ExpectedResponse,
    cluster_authentication: Callable[[], Dict[str, Any]],
):
    _PATCH_EXPORT = {"by_alias": True, "exclude_unset": True, "exclude_none": True}
    # check modifying invalid returns not found
    assert client.app
    url = client.app.router["update_cluster_handler"].url_for(cluster_id=f"{25}")
    rsp = await client.patch(
        f"{url}",
        json=ClusterPatch().dict(**_PATCH_EXPORT),
    )
    data, error = await assert_status(rsp, expected.not_found)
    if error and user_role in [UserRole.ANONYMOUS, UserRole.GUEST]:
        return

    # create our own cluster, allows us to modify it
    admin_cluster: Cluster = await cluster(GroupID(logged_user["primary_gid"]))
    url = client.app.router["update_cluster_handler"].url_for(
        cluster_id=f"{admin_cluster.id}"
    )
    # we can modify these simple things
    expected_admin_cluster = admin_cluster.copy()
    for cluster_patch in [
        ClusterPatch(name="My patched cluster name"),
        ClusterPatch(description="My patched cluster description"),
        ClusterPatch(type=ClusterType.ON_PREMISE),
        ClusterPatch(thumbnail="https://placeimg.com/640/480/nature"),
        ClusterPatch(endpoint="https://some_other_endpoint.com"),
        ClusterPatch(authentication=cluster_authentication()),
    ]:
        jsonable_cluster_patch = json.loads(cluster_patch.json(**_PATCH_EXPORT))
        print(f"--> patching cluster with {jsonable_cluster_patch}")
        rsp = await client.patch(f"{url}", json=jsonable_cluster_patch)
        data, error = await assert_status(rsp, expected.ok)
        expected_admin_cluster = expected_admin_cluster.copy(
            update=cluster_patch.dict(**_PATCH_EXPORT)
        )
        assert Cluster.parse_obj(data) == expected_admin_cluster

    # we can change the access rights, the owner rights are always kept
    for rights in [
        CLUSTER_ADMIN_RIGHTS,
        CLUSTER_MANAGER_RIGHTS,
        CLUSTER_USER_RIGHTS,
        CLUSTER_NO_RIGHTS,
    ]:
        cluster_patch = ClusterPatch(accessRights={second_user["primary_gid"]: rights})
        rsp = await client.patch(
            f"{url}",
            json=cluster_patch.dict(**_PATCH_EXPORT),
        )
        data, error = await assert_status(rsp, expected.ok)
        expected_admin_cluster.access_rights[second_user["primary_gid"]] = rights
        assert Cluster.parse_obj(data) == expected_admin_cluster

    # we can change the owner since we are admin
    cluster_patch = ClusterPatch(owner=second_user["primary_gid"])
    rsp = await client.patch(
        f"{url}",
        json=cluster_patch.dict(**_PATCH_EXPORT),
    )
    data, error = await assert_status(rsp, expected.ok)
    expected_admin_cluster.owner = second_user["primary_gid"]
    expected_admin_cluster.access_rights[
        second_user["primary_gid"]
    ] = CLUSTER_ADMIN_RIGHTS
    assert Cluster.parse_obj(data) == expected_admin_cluster

    # we should not be able to reduce the rights of the new owner
    cluster_patch = ClusterPatch(
        accessRights={second_user["primary_gid"]: CLUSTER_NO_RIGHTS}
    )
    rsp = await client.patch(
        f"{url}",
        json=cluster_patch.dict(**_PATCH_EXPORT),
    )
    data, error = await assert_status(rsp, expected.forbidden)

    # we have a second user that creates a few clusters, some are shared with the first user
    a_cluster_that_may_be_administrated: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
        {GroupID(logged_user["primary_gid"]): CLUSTER_ADMIN_RIGHTS},
    )
    a_cluster_that_may_be_managed: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
        {GroupID(logged_user["primary_gid"]): CLUSTER_MANAGER_RIGHTS},
    )
    a_cluster_that_may_be_used: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
        {GroupID(logged_user["primary_gid"]): CLUSTER_USER_RIGHTS},
    )
    a_cluster_that_is_not_shared: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
    )
    a_cluster_that_may_not_be_used: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
        {
            GroupID(all_group["gid"]): CLUSTER_ADMIN_RIGHTS,
            GroupID(logged_user["primary_gid"]): CLUSTER_NO_RIGHTS,
        },
    )
    # we can manage so we shall be ok here changing the name
    for cl in [a_cluster_that_may_be_administrated, a_cluster_that_may_be_managed]:
        url = client.app.router["update_cluster_handler"].url_for(cluster_id=f"{cl.id}")
        rsp = await client.patch(
            f"{url}",
            json=ClusterPatch(name="I prefer this new name here").dict(**_PATCH_EXPORT),
        )
        data, error = await assert_status(rsp, expected.ok)

    # we can NOT change the owner of this one
    url = client.app.router["update_cluster_handler"].url_for(
        cluster_id=f"{a_cluster_that_may_be_managed.id}"
    )
    rsp = await client.patch(
        f"{url}",
        json=ClusterPatch(owner=logged_user["primary_gid"]).dict(**_PATCH_EXPORT),
    )
    data, error = await assert_status(rsp, expected.forbidden)

    # we can NOT change ourself to become an admin
    url = client.app.router["update_cluster_handler"].url_for(
        cluster_id=f"{a_cluster_that_may_be_managed.id}"
    )
    rsp = await client.patch(
        f"{url}",
        json=ClusterPatch(
            accessRights={logged_user["primary_gid"]: CLUSTER_ADMIN_RIGHTS}
        ).dict(**_PATCH_EXPORT),
    )
    data, error = await assert_status(rsp, expected.forbidden)

    # but I can add a user
    url = client.app.router["update_cluster_handler"].url_for(
        cluster_id=f"{a_cluster_that_may_be_managed.id}"
    )
    rsp = await client.patch(
        f"{url}",
        json=ClusterPatch(
            accessRights={
                **a_cluster_that_may_be_managed.access_rights,
                **{
                    logged_user["primary_gid"]: CLUSTER_MANAGER_RIGHTS,
                    all_group["gid"]: CLUSTER_USER_RIGHTS,
                },
            },
        ).dict(**_PATCH_EXPORT),
    )
    data, error = await assert_status(rsp, expected.ok)

    # and I shall be able to deny a user
    url = client.app.router["update_cluster_handler"].url_for(
        cluster_id=f"{a_cluster_that_may_be_managed.id}"
    )
    rsp = await client.patch(
        f"{url}",
        json=ClusterPatch(
            accessRights={
                logged_user["primary_gid"]: CLUSTER_MANAGER_RIGHTS,
                all_group["gid"]: CLUSTER_NO_RIGHTS,
            }
        ).dict(**_PATCH_EXPORT),
    )
    data, error = await assert_status(rsp, expected.ok)

    # and I shall be able to remove a user (provided that is not the owner)
    url = client.app.router["update_cluster_handler"].url_for(
        cluster_id=f"{a_cluster_that_may_be_managed.id}"
    )
    rsp = await client.patch(
        f"{url}",
        json=ClusterPatch(
            accessRights={
                logged_user["primary_gid"]: CLUSTER_MANAGER_RIGHTS,
            }
        ).dict(**_PATCH_EXPORT),
    )
    data, error = await assert_status(rsp, expected.ok)

    # but I canNOT add a manager or an admin
    for rights in [CLUSTER_ADMIN_RIGHTS, CLUSTER_MANAGER_RIGHTS]:
        url = client.app.router["update_cluster_handler"].url_for(
            cluster_id=f"{a_cluster_that_may_be_managed.id}"
        )
        rsp = await client.patch(
            f"{url}",
            json=ClusterPatch(accessRights={all_group["gid"]: rights}).dict(
                **_PATCH_EXPORT
            ),
        )
        data, error = await assert_status(rsp, expected.forbidden)

    # we can NOT manage so we shall be forbidden changing the name
    for cl in [
        a_cluster_that_may_be_used,
        a_cluster_that_may_not_be_used,
        a_cluster_that_is_not_shared,
    ]:
        url = client.app.router["update_cluster_handler"].url_for(cluster_id=f"{cl.id}")
        rsp = await client.patch(
            f"{url}",
            json=ClusterPatch(
                name="I prefer this new name here, but I am not allowed"
            ).dict(**_PATCH_EXPORT),
        )
        data, error = await assert_status(rsp, expected.forbidden)
Beispiel #12
0
async def test_get_cluster(
    enable_dev_features: None,
    client: TestClient,
    logged_user: Dict[str, Any],
    second_user: Dict[str, Any],
    all_group: Dict[str, Any],
    cluster: Callable[..., Coroutine[Any, Any, Cluster]],
    user_role: UserRole,
    expected: ExpectedResponse,
):
    # check not found
    assert client.app
    url = client.app.router["get_cluster_handler"].url_for(cluster_id=f"{25}")
    rsp = await client.get(f"{url}")
    data, error = await assert_status(rsp, expected.not_found)
    if error and user_role in [UserRole.ANONYMOUS, UserRole.GUEST]:
        return
    assert data is None

    # create our own cluster, and we can get it
    admin_cluster: Cluster = await cluster(GroupID(logged_user["primary_gid"]))
    url = client.app.router["get_cluster_handler"].url_for(
        cluster_id=f"{admin_cluster.id}"
    )
    rsp = await client.get(f"{url}")
    data, error = await assert_status(rsp, expected.ok)
    assert Cluster.parse_obj(data) == admin_cluster

    # we have a second user that creates a few clusters, some are shared with the first user
    a_cluster_that_may_be_administrated: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
        {GroupID(logged_user["primary_gid"]): CLUSTER_MANAGER_RIGHTS},
    )
    a_cluster_that_may_be_managed: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
        {GroupID(logged_user["primary_gid"]): CLUSTER_MANAGER_RIGHTS},
    )
    a_cluster_that_may_be_used: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
        {GroupID(logged_user["primary_gid"]): CLUSTER_USER_RIGHTS},
    )
    a_cluster_that_is_not_shared: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
    )
    a_cluster_that_may_not_be_used: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
        {
            GroupID(all_group["gid"]): CLUSTER_USER_RIGHTS,
            GroupID(logged_user["primary_gid"]): CLUSTER_NO_RIGHTS,
        },
    )

    # we should have access to that one
    for cl in [
        a_cluster_that_may_be_administrated,
        a_cluster_that_may_be_managed,
        a_cluster_that_may_be_used,
    ]:
        url = client.app.router["get_cluster_handler"].url_for(cluster_id=f"{cl.id}")
        rsp = await client.get(f"{url}")
        data, error = await assert_status(rsp, expected.ok)
        assert Cluster.parse_obj(data) == cl

    # we should not have access to these
    for cl in [a_cluster_that_is_not_shared, a_cluster_that_may_not_be_used]:
        url = client.app.router["get_cluster_handler"].url_for(cluster_id=f"{cl.id}")
        rsp = await client.get(f"{url}")
        data, error = await assert_status(rsp, expected.forbidden)
Beispiel #13
0
async def test_list_clusters(
    enable_dev_features: None,
    client: TestClient,
    logged_user: Dict[str, Any],
    second_user: Dict[str, Any],
    all_group: Dict[str, Any],
    cluster: Callable[..., Coroutine[Any, Any, Cluster]],
    expected: ExpectedResponse,
):
    # check empty clusters
    assert client.app
    url = client.app.router["list_clusters_handler"].url_for()
    rsp = await client.get(f"{url}")
    data, error = await assert_status(rsp, expected.ok)
    if error:
        # we are done here, anonymous and guests cannot list
        return
    assert data == []

    # create our own cluster, and check it is listed
    admin_cluster: Cluster = await cluster(GroupID(logged_user["primary_gid"]))
    rsp = await client.get(f"{url}")
    data, error = await assert_status(rsp, expected.ok)
    assert len(data) == 1
    assert Cluster.parse_obj(data[0]) == admin_cluster

    # we have a second user that creates a few clusters, some are shared with the first user
    a_cluster_that_may_be_administred: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
        {GroupID(logged_user["primary_gid"]): CLUSTER_MANAGER_RIGHTS},
    )
    a_cluster_that_may_be_managed: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
        {GroupID(logged_user["primary_gid"]): CLUSTER_MANAGER_RIGHTS},
    )

    a_cluster_that_may_be_used: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
        {GroupID(logged_user["primary_gid"]): CLUSTER_USER_RIGHTS},
    )

    a_cluster_that_is_not_shared: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
    )

    a_cluster_that_may_not_be_used: Cluster = await cluster(
        GroupID(second_user["primary_gid"]),
        {
            GroupID(all_group["gid"]): CLUSTER_USER_RIGHTS,
            GroupID(logged_user["primary_gid"]): CLUSTER_NO_RIGHTS,
        },
    )

    # now listing should retrieve both clusters
    rsp = await client.get(f"{url}")
    data, error = await assert_status(rsp, expected.ok)
    assert len(data) == (1 + 3)
    for d in data:
        assert Cluster.parse_obj(d) in [
            admin_cluster,
            a_cluster_that_may_be_administred,
            a_cluster_that_may_be_managed,
            a_cluster_that_may_be_used,
        ]