From b5c837e29895469643e3bdb74a21633dbc221ba2 Mon Sep 17 00:00:00 2001
From: tolstislon <tolstislon@gmail.com>
Date: Sun, 24 Sep 2023 19:45:39 +0300
Subject: [PATCH] Add support for Groups API methods

---
 .editorconfig                 |  2 +-
 testrail_api/_category.py     | 51 +++++++++++++++++++
 testrail_api/_testrail_api.py | 10 +++-
 tests/test_groups.py          | 93 +++++++++++++++++++++++++++++++++++
 4 files changed, 154 insertions(+), 2 deletions(-)
 create mode 100644 tests/test_groups.py

diff --git a/.editorconfig b/.editorconfig
index 5c3381f..9c87de4 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -10,7 +10,7 @@ indent_size = 4
 
 # Python files
 [*.py]
-max_line_length = 88
+max_line_length = 120
 ij_python_optimize_imports_always_split_from_imports = false
 ij_python_optimize_imports_case_insensitive_order = false
 ij_python_optimize_imports_join_from_imports_with_same_source = true
diff --git a/testrail_api/_category.py b/testrail_api/_category.py
index bde0d2a..274e390 100644
--- a/testrail_api/_category.py
+++ b/testrail_api/_category.py
@@ -2297,3 +2297,54 @@ class Roles(_MetaCategory):
     def get_roles(self) -> Dict[str, Any]:
         """Returns a list of available roles"""
         return self.s.get(endpoint="get_roles")
+
+
+class Groups(_MetaCategory):
+    """https://support.testrail.com/hc/en-us/articles/7077338821012-Groups"""
+
+    def get_group(self, group_id: int) -> dict:
+        """
+        Returns an existing group.
+
+        :param group_id: int
+            The ID of the group
+        """
+        return self.s.get(f"get_group/{group_id}")
+
+    def get_groups(self) -> Dict[str, Any]:
+        """Returns the list of available groups."""
+        return self.s.get("get_groups")
+
+    def add_group(self, name: str, user_ids: List[int]) -> dict:
+        """
+        Creates a new group.
+
+        :param name: str
+            The name of the group
+        :param user_ids: list[int]
+            An array of user IDs. Each ID is a user belonging to this group
+        """
+        return self.s.post("add_group", json={"name": name, "user_ids": user_ids})
+
+    def update_group(self, group_id: int, **kwargs) -> dict:
+        """
+        Updates an existing group.
+
+        :param group_id: int
+            The ID of the group
+        :param kwargs:
+            :key name: str
+                The name of the group
+            :key user_ids: list[int]
+                An array of user IDs. Each ID is a user belonging to this group
+        """
+        return self.s.post(f"update_group/{group_id}", json=kwargs)
+
+    def delete_group(self, group_id: int) -> None:
+        """
+        Deletes an existing group.
+
+        :param group_id: int
+            The ID of the group
+        """
+        return self.s.post(f"delete_group/{group_id}")
diff --git a/testrail_api/_testrail_api.py b/testrail_api/_testrail_api.py
index 21f449e..b275e34 100644
--- a/testrail_api/_testrail_api.py
+++ b/testrail_api/_testrail_api.py
@@ -186,7 +186,15 @@ def users(self) -> _category.Users:
     @property
     def roles(self) -> _category.Roles:
         """
-        https://support.testrail.com/hc/en-us/articles/7077853258772-Roles#getroles
+        https://support.testrail.com/hc/en-us/articles/7077853258772-Roles
         Use the following API methods to request details about roles.
         """
         return _category.Roles(self)
+
+    @property
+    def groups(self) -> _category.Groups:
+        """
+        https://support.testrail.com/hc/en-us/articles/7077338821012-Groups
+        Use the following API methods to request details about groups.
+        """
+        return _category.Groups(self)
diff --git a/tests/test_groups.py b/tests/test_groups.py
new file mode 100644
index 0000000..9190e8d
--- /dev/null
+++ b/tests/test_groups.py
@@ -0,0 +1,93 @@
+import responses
+import json
+import pytest
+
+
+def get_group(r):
+    return 200, {}, json.dumps({
+        "id": 1,
+        "name": "New group",
+        "user_ids": [1, 2, 3, 4, 5]
+    })
+
+
+def get_groups(r):
+    return 200, {}, json.dumps({
+        "offset": 0,
+        "limit": 250,
+        "size": 0,
+        "_links": {
+            "next": None,
+            "prev": None,
+        },
+        "groups": [
+            {
+                "id": 1,
+                "name": "New group",
+                "user_ids": [1, 2, 3, 4, 5]
+            }
+        ]
+    })
+
+
+def add_group(r):
+    req = json.loads(r.body)
+    req['id'] = 1
+    return 200, {}, json.dumps(req)
+
+
+def test_get_group(api, mock, url):
+    mock.add_callback(
+        responses.GET,
+        url('get_group/1'),
+        get_group,
+        content_type='application/json'
+    )
+    resp = api.groups.get_group(1)
+    assert resp["id"] == 1
+
+
+def test_get_groups(api, mock, url):
+    mock.add_callback(
+        responses.GET,
+        url('get_groups'),
+        get_groups,
+        content_type='application/json'
+    )
+    resp = api.groups.get_groups()
+    assert resp["groups"][0]["id"] == 1
+
+
+def test_add_group(api, mock, url):
+    mock.add_callback(
+        responses.POST,
+        url('add_group'),
+        add_group,
+        content_type='application/json'
+    )
+    resp = api.groups.add_group("New group", [1, 2, 3, 4])
+    assert resp["id"] == 1
+
+
+@pytest.mark.parametrize("data", ({"name": "qwe"}, {"user_ids": [1, 2]}, {"name": "q", "user_ids": [1, 3]}))
+def test_update_group(api, mock, url, data):
+    mock.add_callback(
+        responses.POST,
+        url('update_group/1'),
+        add_group,
+        content_type='application/json'
+    )
+    resp = api.groups.update_group(1, **data)
+    for key in data:
+        assert resp[key] == data[key]
+
+
+def test_delete_group(api, mock, url):
+    mock.add_callback(
+        responses.POST,
+        url('delete_group/1'),
+        lambda x: (200, {}, ''),
+        content_type='application/json'
+    )
+    response = api.groups.delete_group(1)
+    assert response is None