Repository: fractal-analytics-platform/fractal-client (language: python)

Issue:
Align with new `set-groups` endpoint
See https://github.com/fractal-analytics-platform/fractal-server/pull/2106
Requires fractal-server 2.9.0a11


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 605d8195..8611ffe3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@
 * Remove (internal) obsolete `do_not_separate_logs` argument (\#738).
 * Add `group {add|remove}-user` commands, and deprecate `--new-user-ids` argument from `group update` (\#748).
 * Update `user whoami --viewer-paths` to call the new dedicated [server endpoint](https://github.com/fractal-analytics-platform/fractal-server/pull/2096) (\#748).
+* Add `user set-groups` commands (\#753).
 * Testing:
     * Align with fractal-server 2.9.0 removal of `DB_ENGINE` variable (\#743).
 
diff --git a/fractal_client/cmd/__init__.py b/fractal_client/cmd/__init__.py
index 35d2be2a..3a8690a2 100644
--- a/fractal_client/cmd/__init__.py
+++ b/fractal_client/cmd/__init__.py
@@ -33,6 +33,7 @@
 from ._user import user_edit
 from ._user import user_list
 from ._user import user_register
+from ._user import user_set_groups
 from ._user import user_show
 from ._user import user_whoami
 from ._workflow import delete_workflow
@@ -361,6 +362,13 @@ def user(
         ]
         function_kwargs = get_kwargs(parameters, kwargs)
         iface = user_edit(client, **function_kwargs)
+    elif subcmd == "set-groups":
+        parameters = [
+            "user_id",
+            "group_ids",
+        ]
+        function_kwargs = get_kwargs(parameters, kwargs)
+        iface = user_set_groups(client, **function_kwargs)
     elif subcmd == "whoami":
         parameters = ["viewer_paths"]
         function_kwargs = get_kwargs(parameters, kwargs)
diff --git a/fractal_client/cmd/_user.py b/fractal_client/cmd/_user.py
index 2a6f57bc..e925ee1a 100644
--- a/fractal_client/cmd/_user.py
+++ b/fractal_client/cmd/_user.py
@@ -205,6 +205,17 @@ def user_edit(
     return Interface(retcode=0, data=new_user_with_settings)
 
 
+def user_set_groups(
+    client: AuthClient, *, user_id: int, group_ids: list[int]
+) -> Interface:
+    res = client.post(
+        f"{settings.FRACTAL_SERVER}/auth/users/{user_id}/set-groups/",
+        json=dict(group_ids=group_ids),
+    )
+    user = check_response(res, expected_status_code=200)
+    return Interface(retcode=0, data=user)
+
+
 def user_whoami(
     client: AuthClient, *, batch: bool, viewer_paths: bool = False
 ) -> Interface:
diff --git a/fractal_client/parser.py b/fractal_client/parser.py
index 55501fe3..ee0e8614 100644
--- a/fractal_client/parser.py
+++ b/fractal_client/parser.py
@@ -912,6 +912,27 @@
     required=False,
 )
 
+# user set-groups
+user_set_groups_parser = user_subparsers.add_parser(
+    "set-groups",
+    description=(
+        "Reset user-group membership for an existing user."
+    ),
+    allow_abbrev=False,
+)
+user_set_groups_parser.add_argument(
+    "user_id", help="ID of the user.", type=int
+)
+user_set_groups_parser.add_argument(
+    "group_ids",
+    help=(
+        "List of the IDs of groups we want the user to be member. "
+        "WARNING: this list replaces the current group memberships."
+    ),
+    type=int,
+    nargs="+",
+)
+
 
 # (USER)GROUP GROUP
 
diff --git a/poetry.lock b/poetry.lock
index 7520f898..c86d5112 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -696,7 +696,7 @@ typing = ["typing-extensions (>=4.12.2)"]
 
 [[package]]
 name = "fractal-server"
-version = "2.9.0a10"
+version = "2.9.0a11"
 description = "Server component of the Fractal analytics platform"
 optional = false
 python-versions = "^3.10"
@@ -726,7 +726,7 @@ uvicorn-worker = "^0.2.0"
 type = "git"
 url = "https://github.com/fractal-analytics-platform/fractal-server.git"
 reference = "main"
-resolved_reference = "8bb57912219c0b87997249d730c7f0413dcf8478"
+resolved_reference = "0ea8af11f425d4d32aa5e7bde90bf9fdd6105762"
 
 [[package]]
 name = "ghp-import"


Test patch:
diff --git a/tests/test_group.py b/tests/test_group.py
index 4ba36d7a..17a33f25 100644
--- a/tests/test_group.py
+++ b/tests/test_group.py
@@ -113,14 +113,18 @@ def test_group_commands(
 
     res = invoke_as_superuser("group list --user-ids")
     assert len(res.data) == initial_number_of_groups + 2
-    assert res.data[0]["id"] == default_group_id
-    assert len(res.data[0]["user_ids"]) == initial_number_of_users + 3
-    assert res.data[-2]["id"] == group1_id
-    assert set(res.data[1]["user_ids"]) == set([user1_id, user2_id])
-    assert res.data[-1]["id"] == group2_id
-    assert set(res.data[2]["user_ids"]) == set(
-        [user3_id, user2_id, superuser_id]
+    users_default_group = next(
+        g["user_ids"] for g in res.data if g["id"] == default_group_id
+    )
+    assert len(users_default_group) == initial_number_of_users + 3
+    users_group_1 = next(
+        g["user_ids"] for g in res.data if g["id"] == group1_id
+    )
+    assert set(users_group_1) == set([user1_id, user2_id])
+    users_group_2 = next(
+        g["user_ids"] for g in res.data if g["id"] == group2_id
     )
+    assert set(users_group_2) == set([user3_id, user2_id, superuser_id])
 
     # Remove users from group
     res = invoke_as_superuser(f"group remove-user {group2_id} {user3_id}")
diff --git a/tests/test_user.py b/tests/test_user.py
index cce851da..ea5e4c59 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -2,6 +2,7 @@
 
 import pytest
 from devtools import debug
+from fractal_server.app.security import FRACTAL_DEFAULT_GROUP_NAME
 
 PWD_USER = "1234"
 
@@ -383,3 +384,76 @@ def test_whoami_as_superuser(invoke_as_superuser, superuser):
     debug(res.data)
     assert res.data["email"] == superuser["email"]
     assert res.data["is_superuser"]
+
+
+def test_user_set_groups(invoke_as_superuser, user_factory, new_name):
+    # get default group
+    res = invoke_as_superuser("group list --user-ids")
+    default_group = next(
+        group
+        for group in res.data
+        if group["name"] == FRACTAL_DEFAULT_GROUP_NAME
+    )
+    default_group_id = default_group["id"]
+    # create 2 new users
+    user1 = user_factory(email=f"{new_name()}@example.org", password="psw1")
+    assert len(user1["group_ids_names"]) == 1
+    user1_id = user1["id"]
+    user2 = user_factory(email=f"{new_name()}@example.org", password="psw2")
+    assert len(user2["group_ids_names"]) == 1
+    user2_id = user2["id"]
+    # create 2 new groups
+    group1 = invoke_as_superuser(f"group new {new_name()}")
+    assert len(group1.data["user_ids"]) == 0
+    group1_id = group1.data["id"]
+    group2 = invoke_as_superuser(f"group new {new_name()}")
+    assert len(group2.data["user_ids"]) == 0
+    group2_id = group2.data["id"]
+
+    with pytest.raises(SystemExit):
+        # no arguments
+        invoke_as_superuser("user set-groups")
+    with pytest.raises(SystemExit):
+        # no group_ids list
+        invoke_as_superuser(f"user set-groups {user1_id}")
+    with pytest.raises(SystemExit):
+        # group_ids must be a list of integers
+        invoke_as_superuser(f"user set-groups {user1_id} {group1_id} foo")
+    with pytest.raises(SystemExit):
+        # there must always be the default group id in group_ids
+        invoke_as_superuser(f"user set-groups {user1_id} {group1_id}")
+    with pytest.raises(SystemExit):
+        # repeated elements in group_ids are forbidden
+        invoke_as_superuser(
+            "user set-groups "
+            f"{user1_id} {default_group_id} {group1_id} {group1_id}"
+        )
+
+    # Add user1 to group1
+    res = invoke_as_superuser(
+        f"user set-groups {user1_id} {group1_id} {default_group_id}"
+    )
+    assert len(res.data["group_ids_names"]) == 2
+    group1 = invoke_as_superuser(f"group get {group1_id}")
+    assert len(group1.data["user_ids"]) == 1
+
+    # Add user2 to group1 and group2
+    res = invoke_as_superuser(
+        "user set-groups "
+        f"{user2_id} {group2_id} {group1_id} {default_group_id}"
+    )
+    assert len(res.data["group_ids_names"]) == 3
+    group1 = invoke_as_superuser(f"group get {group1_id}")
+    assert len(group1.data["user_ids"]) == 2
+    group2 = invoke_as_superuser(f"group get {group2_id}")
+    assert len(group2.data["user_ids"]) == 1
+
+    # Add user1 to group2 and remove them from group1
+    res = invoke_as_superuser(
+        f"user set-groups {user1_id} {group2_id} {default_group_id}"
+    )
+    assert len(res.data["group_ids_names"]) == 2
+    group1 = invoke_as_superuser(f"group get {group1_id}")
+    assert len(group1.data["user_ids"]) == 1
+    group2 = invoke_as_superuser(f"group get {group2_id}")
+    assert len(group2.data["user_ids"]) == 2