Skip to content

Commit c92a145

Browse files
committed
Address reviews
- add project and workspace role enums - add optional username to create_user - remove redudant arguments from delete() method - add tests for failures - permission issue, duplicate call, editor limit hit
1 parent e528c5b commit c92a145

File tree

3 files changed

+97
-40
lines changed

3 files changed

+97
-40
lines changed

mergin/client.py

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import typing
1818
import warnings
1919

20-
from .common import ClientError, LoginError
20+
from .common import ClientError, LoginError, WorkspaceRole, ProjectRole
2121
from .merginproject import MerginProject
2222
from .client_pull import (
2323
download_file_finalize,
@@ -245,9 +245,9 @@ def patch(self, path, data=None, headers={}):
245245
request = urllib.request.Request(url, data, headers, method="PATCH")
246246
return self._do_request(request)
247247

248-
def delete(self, path, data=None, headers={}):
248+
def delete(self, path):
249249
url = urllib.parse.urljoin(self.url, urllib.parse.quote(path))
250-
request = urllib.request.Request(url, data, headers, method="DELETE")
250+
request = urllib.request.Request(url, method="DELETE")
251251
return self._do_request(request)
252252

253253
def login(self, login, password):
@@ -1235,22 +1235,28 @@ def has_editor_support(self):
12351235
"""
12361236
return is_version_acceptable(self.server_version(), "2024.4.0")
12371237

1238-
def create_user(self, email: str, password: str, workspace_id: int, workspace_role: str, notify_user: bool = False):
1238+
def create_user(
1239+
self,
1240+
email: str,
1241+
password: str,
1242+
workspace_id: int,
1243+
workspace_role: WorkspaceRole,
1244+
username: str = None,
1245+
notify_user: bool = False,
1246+
):
12391247
"""
12401248
Create a new user in a workspace. The username is generated from the email address.
12411249
"""
12421250
params = {
12431251
"email": email,
12441252
"password": password,
12451253
"workspace_id": workspace_id,
1246-
"role": workspace_role,
1254+
"role": workspace_role.value,
12471255
"notify_user": notify_user,
12481256
}
1249-
try:
1250-
self.post("v2/users", params, json_headers)
1251-
except ClientError as e:
1252-
e.extra = f"Email: {email}"
1253-
raise e
1257+
if username:
1258+
params["username"] = username
1259+
self.post("v2/users", params, json_headers)
12541260

12551261
def get_workspace_member(self, workspace_id: int, user_id: int):
12561262
"""
@@ -1267,14 +1273,14 @@ def list_workspace_members(self, workspace_id: int):
12671273
return json.load(resp)
12681274

12691275
def update_workspace_member(
1270-
self, workspace_id: int, user_id: int, workspace_role: str, reset_projects_roles: bool = False
1276+
self, workspace_id: int, user_id: int, workspace_role: WorkspaceRole, reset_projects_roles: bool = False
12711277
):
12721278
"""
12731279
Update workspace role of a workspace member, optionally resets the projects role
12741280
"""
12751281
params = {
12761282
"reset_projects_roles": reset_projects_roles,
1277-
"workspace_role": workspace_role,
1283+
"workspace_role": workspace_role.value,
12781284
}
12791285
resp = self.patch(f"v2/workspaces/{workspace_id}/members/{user_id}", params, json_headers)
12801286
return json.load(resp)
@@ -1292,19 +1298,19 @@ def list_project_collaborators(self, project_id: int):
12921298
project_collaborators = self.get(f"v2/projects/{project_id}/collaborators")
12931299
return json.load(project_collaborators)
12941300

1295-
def add_project_collaborator(self, project_id: int, user: str, project_role: str):
1301+
def add_project_collaborator(self, project_id: int, user: str, project_role: ProjectRole):
12961302
"""
12971303
Add a user to project collaborators and grant them a project role
12981304
"""
1299-
params = {"role": project_role, "user": user}
1305+
params = {"role": project_role.value, "user": user}
13001306
project_collaborator = self.post(f"v2/projects/{project_id}/collaborators", params, json_headers)
13011307
return json.load(project_collaborator)
13021308

1303-
def update_project_collaborator(self, project_id: int, user_id: int, project_role: str):
1309+
def update_project_collaborator(self, project_id: int, user_id: int, project_role: ProjectRole):
13041310
"""
13051311
Update project role of the existing project collaborator
13061312
"""
1307-
params = {"role": project_role}
1313+
params = {"role": project_role.value}
13081314
project_collaborator = self.patch(f"v2/projects/{project_id}/collaborators/{user_id}", params, json_headers)
13091315
return json.load(project_collaborator)
13101316

mergin/common.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,27 @@ class InvalidProject(Exception):
6565

6666
import dateutil.parser
6767
from dateutil.tz import tzlocal
68+
69+
70+
class WorkspaceRole(Enum):
71+
"""
72+
Workspace roles
73+
"""
74+
75+
GUEST = "guest"
76+
READER = "reader"
77+
EDITOR = "editor"
78+
WRITER = "writer"
79+
ADMIN = "admin"
80+
OWNER = "owner"
81+
82+
83+
class ProjectRole(Enum):
84+
"""
85+
Project roles
86+
"""
87+
88+
READER = "reader"
89+
EDITOR = "editor"
90+
WRITER = "writer"
91+
OWNER = "owner"

mergin/test/test_client.py

Lines changed: 51 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
from ..merginproject import pygeodiff
4040
from ..report import create_report
4141
from ..editor import EDITOR_ROLE_NAME, filter_changes, is_editor_enabled
42-
from ..common import ErrorCode
42+
from ..common import ErrorCode, WorkspaceRole, ProjectRole
4343

4444
SERVER_URL = os.environ.get("TEST_MERGIN_URL")
4545
API_USER = os.environ.get("TEST_API_USERNAME")
@@ -51,6 +51,8 @@
5151
CHANGED_SCHEMA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), "modified_schema")
5252
STORAGE_WORKSPACE = os.environ.get("TEST_STORAGE_WORKSPACE", "testpluginstorage")
5353

54+
json_headers = {"Content-Type": "application/json"}
55+
5456

5557
def get_limit_overrides(storage: int):
5658
return {"storage": storage, "projects": 2, "api_allowed": True}
@@ -2744,34 +2746,37 @@ def test_workspace_requests(mc2: MerginClient):
27442746
assert service["subscription"] == None
27452747

27462748

2747-
def test_access_management(mc: MerginClient):
2748-
# create a user in a workspace
2749-
workspace_id = None
2750-
for workspace in mc.workspaces_list():
2751-
if workspace["name"] == mc.username():
2752-
workspace_id = workspace["id"]
2753-
break
2749+
def test_access_management(mc: MerginClient, mc2: MerginClient):
2750+
# create a user in the workspace
2751+
workspace_id = next((w["id"] for w in mc.workspaces_list() if w["name"] == mc.username()))
27542752
email = "create_user" + str(random.randint(1000, 9999)) + "@client.py"
27552753
password = "Il0vemergin"
2756-
role = "writer"
2757-
mc.create_user(email, password, workspace_id, role)
2754+
ws_role = WorkspaceRole.WRITER
2755+
mc.create_user(email, password, workspace_id, ws_role)
27582756
workspace_members = mc.list_workspace_members(workspace_id)
27592757
new_user = next((m for m in workspace_members if m["email"] == email))
27602758
assert new_user
2761-
assert new_user["workspace_role"] == role
2762-
# test get workspace member
2759+
assert new_user["workspace_role"] == ws_role.value
2760+
# get workspace member
27632761
ws_member = mc.get_workspace_member(workspace_id, new_user["id"])
27642762
assert ws_member["email"] == email
2765-
assert ws_member["workspace_role"] == role
2766-
updated_role = "admin"
2767-
# test update workspace member
2763+
assert ws_member["workspace_role"] == ws_role.value
2764+
updated_role = WorkspaceRole.ADMIN
2765+
# update workspace member
27682766
mc.update_workspace_member(workspace_id, new_user["id"], updated_role)
27692767
updated_user = mc.get_workspace_member(workspace_id, new_user["id"])
2770-
assert updated_user["workspace_role"] == updated_role
2771-
# test remove workspace member
2768+
assert updated_user["workspace_role"] == updated_role.value
2769+
# test permissions - a different client cannot update the role
2770+
with pytest.raises(ClientError, match=f"You do not have admin permissions to workspace"):
2771+
mc2.update_workspace_member(workspace_id, new_user["id"], ws_role)
2772+
# remove workspace member
27722773
mc.remove_workspace_member(workspace_id, new_user["id"])
27732774
workspace_members = mc.list_workspace_members(workspace_id)
27742775
assert not any(m["id"] == new_user["id"] for m in workspace_members)
2776+
# duplicated call
2777+
with pytest.raises(ClientError) as exc_info:
2778+
mc.remove_workspace_member(workspace_id, new_user["id"])
2779+
assert exc_info.value.http_error == 404
27752780
# add project
27762781
test_project_name = "test_collaborators"
27772782
test_project_fullname = API_USER + "/" + test_project_name
@@ -2780,20 +2785,42 @@ def test_access_management(mc: MerginClient):
27802785
mc.create_project(test_project_name)
27812786
project_info = get_project_info(mc, API_USER, test_project_name)
27822787
test_project_id = project_info["id"]
2783-
project_role = "reader"
2784-
# test add project collaborator
2788+
project_role = ProjectRole.READER
2789+
# user must be added to project collaborators before updating project role
2790+
updated_role = ProjectRole.OWNER
2791+
with pytest.raises(ClientError) as exc_info2:
2792+
mc.update_project_collaborator(test_project_id, new_user["id"], updated_role)
2793+
assert exc_info2.value.http_error == 404
2794+
# add project collaborator
27852795
mc.add_project_collaborator(test_project_id, new_user["email"], project_role)
27862796
collaborators = mc.list_project_collaborators(test_project_id)
27872797
new_collaborator = next((c for c in collaborators if c["id"] == new_user["id"]))
27882798
assert new_collaborator
2789-
assert new_collaborator["project_role"] == project_role
2790-
updated_role = "owner"
2791-
# test update project collaborator
2799+
assert new_collaborator["project_role"] == project_role.value
2800+
# update project collaborator
27922801
mc.update_project_collaborator(test_project_id, new_user["id"], updated_role)
27932802
collaborators = mc.list_project_collaborators(test_project_id)
27942803
updated_collaborator = next((c for c in collaborators if c["id"] == new_user["id"]))
2795-
assert updated_collaborator["project_role"] == updated_role
2796-
# test remove project collaborator
2804+
assert updated_collaborator["project_role"] == updated_role.value
2805+
# remove project collaborator
27972806
mc.remove_project_collaborator(test_project_id, new_user["id"])
27982807
collaborators = mc.list_project_collaborators(test_project_id)
27992808
assert not any(c["id"] == new_user["id"] for c in collaborators)
2809+
# try to assign new editor when editors limit is reached
2810+
ws_usage = mc.workspace_usage(workspace_id)
2811+
editors_usage = ws_usage["editors"]["editors_count"] + ws_usage["editors"]["invitations_count"]
2812+
mc.patch(
2813+
f"/v1/tests/workspaces/{workspace_id}",
2814+
{"limits_override": {"editors": editors_usage}},
2815+
json_headers,
2816+
)
2817+
editor_role = ProjectRole.EDITOR
2818+
with pytest.raises(ClientError, match="Maximum number of editors in this workspace is reached."):
2819+
mc.add_project_collaborator(test_project_id, new_user["email"], editor_role)
2820+
# set limits to the original state
2821+
orig_projects_limit = ws_usage["projects"]["quota"]
2822+
mc.patch(
2823+
f"/v1/tests/workspaces/{workspace_id}",
2824+
{"limits_override": {"projects": orig_projects_limit}},
2825+
json_headers,
2826+
)

0 commit comments

Comments
 (0)