From cc690f969fa8f6d0ab1c5edc98dcfe5044c98d6e Mon Sep 17 00:00:00 2001 From: Simon Hammes Date: Wed, 16 Sep 2026 15:08:32 +0200 Subject: [PATCH] Fix department endpoints + add tests --- system_admin_account_operations.yaml | 578 ++++++++++++++---- .../test_departments/test_addDepartment.json | 9 + .../test_addUserToDepartments.json | 23 + .../test_deleteDepartment.json | 3 + .../test_department_group[create].json | 4 + .../test_department_group[delete].json | 3 + .../test_department_members[add].json | 11 + .../test_department_members[list].json | 11 + .../test_department_members[remove].json | 3 + .../test_department_members[update].json | 9 + .../test_listDepartments_top_level.json | 11 + .../test_listNonDepartmentUsers.json | 6 + .../test_updateDepartment.json | 9 + .../test_department_members[add].json | 16 + .../test_department_members[remove].json | 3 + .../test_department_members[update].json | 11 + .../test_getDepartmentGroupMembersCount.json | 3 + .../test_listDepartmentMemberBases.json | 18 + .../test_listDepartmentMembers.json | 13 + .../test_listDepartments.json | 18 + .../test_listSubDepartments.json | 11 + .../test_listUserDepartments.json | 12 + tests/conftest.py | 63 ++ tests/test_departments.py | 298 +++++++++ tests/test_user_departments.py | 257 ++++++++ user_account_operations.yaml | 324 +++++++++- 26 files changed, 1587 insertions(+), 140 deletions(-) create mode 100644 tests/__snapshots__/test_departments/test_addDepartment.json create mode 100644 tests/__snapshots__/test_departments/test_addUserToDepartments.json create mode 100644 tests/__snapshots__/test_departments/test_deleteDepartment.json create mode 100644 tests/__snapshots__/test_departments/test_department_group[create].json create mode 100644 tests/__snapshots__/test_departments/test_department_group[delete].json create mode 100644 tests/__snapshots__/test_departments/test_department_members[add].json create mode 100644 tests/__snapshots__/test_departments/test_department_members[list].json create mode 100644 tests/__snapshots__/test_departments/test_department_members[remove].json create mode 100644 tests/__snapshots__/test_departments/test_department_members[update].json create mode 100644 tests/__snapshots__/test_departments/test_listDepartments_top_level.json create mode 100644 tests/__snapshots__/test_departments/test_listNonDepartmentUsers.json create mode 100644 tests/__snapshots__/test_departments/test_updateDepartment.json create mode 100644 tests/__snapshots__/test_user_departments/test_department_members[add].json create mode 100644 tests/__snapshots__/test_user_departments/test_department_members[remove].json create mode 100644 tests/__snapshots__/test_user_departments/test_department_members[update].json create mode 100644 tests/__snapshots__/test_user_departments/test_getDepartmentGroupMembersCount.json create mode 100644 tests/__snapshots__/test_user_departments/test_listDepartmentMemberBases.json create mode 100644 tests/__snapshots__/test_user_departments/test_listDepartmentMembers.json create mode 100644 tests/__snapshots__/test_user_departments/test_listDepartments.json create mode 100644 tests/__snapshots__/test_user_departments/test_listSubDepartments.json create mode 100644 tests/__snapshots__/test_user_departments/test_listUserDepartments.json create mode 100644 tests/test_departments.py create mode 100644 tests/test_user_departments.py diff --git a/system_admin_account_operations.yaml b/system_admin_account_operations.yaml index 1faf561..698defa 100644 --- a/system_admin_account_operations.yaml +++ b/system_admin_account_operations.yaml @@ -103,13 +103,6 @@ components: required: true description: The unique identifier of a base. Sometimes also called dtable_uuid. example: 5c264e76-0e5a-448a-9f34-580b551364ca - return_ancestors: - name: return_ancestors - in: query - description: Whether to include ancestor groups in the response. - schema: - type: boolean - example: true starting_time: name: start in: query @@ -329,14 +322,24 @@ components: The ID of the invalid synchronization, retrievable from the previous call. example: 2 - parent_department_id: - name: parent_department_id - in: path + parent_id_query: + name: parent_id + in: query schema: type: integer - required: true - description: Optional. -1 by default. + minimum: -1 + description: >- + The ID of the parent department. Optional. Without it (or with `-1`), + the top-level department is returned. example: 1 + department_org_id_query: + name: org_id + in: query + schema: + type: integer + minimum: -1 + description: The ID of the team. Optional. `-1` (system-level departments) by default. + example: -1 department_id: name: department_id in: path @@ -531,10 +534,28 @@ components: group_quota: type: integer description: The quota in MB. - parent_group: + department_name: type: string - description: The ID of the parent department. Optional. -1 by default. + description: The name of the department. Has to be unique within the parent department. + example: Developers + department_parent_id: + type: integer + description: The ID of the parent department. Use `-1` to create the top-level department. + example: 1 + department_org_id: + type: integer + description: The ID of the team. Optional. `-1` (system-level department) by default. example: -1 + department_is_staff: + type: boolean + description: "`true` if the user should be an administrator of the department." + example: true + department_ids: + type: array + description: The IDs of the departments the user should be added to. + items: + type: integer + example: [2, 3] handled: type: boolean description: >- @@ -2781,22 +2802,27 @@ paths: success: true # Departments - /api/v2.1/admin/address-book/groups/{parent_department_id}/: + /api/v2.1/admin/address-book-v2/departments/: get: tags: - Departments summary: List Departments operationId: listDepartments description: >- - List all the departments in the current level. - The parameter `parent_department_id` in the URL is optional. If not - given, the default of `-1` is taken. - In this example, the parent department has the ID of `1`, and there are - two departments with the IDs of `2` and `3` in it. + List the departments on one level of the department tree. Without + `parent_id` (or with `parent_id=-1`), the top-level department is + returned. With `parent_id`, the sub-departments of that department are + returned. + + + Departments of a team are listed by passing the team's `org_id`. + Without `org_id`, the system-level departments (`org_id` of `-1`) are + returned. security: - AccountTokenAuth: [] parameters: - - $ref: "#/components/parameters/parent_department_id" + - $ref: "#/components/parameters/parent_id_query" + - $ref: "#/components/parameters/department_org_id_query" responses: "200": description: OK @@ -2805,71 +2831,50 @@ paths: schema: type: object properties: - id: - type: integer - name: - type: string - owner: - type: string - created_at: - type: string - parent_group_id: - type: integer - quota: - type: integer - groups: - type: array - items: - type: object - members: - type: array - items: - type: object - ancestor_groups: + department_list: type: array items: type: object example: - id: 1 - name: Sys Dev - owner: system admin - created_at: "2021-02-26T11:21:19+00:00" - parent_group_id: -1 - quota: -2 - groups: + department_list: - id: 2 name: Developers - owner: system admin - created_at: "2021-02-26T13:53:44+00:00" - parent_group_id: 1 - quota: -2 + parent_id: 1 + org_id: -1 + id_in_org: 2 - id: 3 name: Test Department - owner: system admin - created_at: "2021-02-26T14:17:06+00:00" - parent_group_id: 1 - quota: -2 - members: [] - ancestor_groups: [] - /api/v2.1/admin/address-book/groups/: + parent_id: 1 + org_id: -1 + id_in_org: 3 post: tags: - Departments summary: Add Department operationId: addDepartment description: >- - Add a new department with a desired name and, by optional, in a parent - department. + Add a new department with a desired name below a parent department. + + + There can be only one top-level department (`parent_id` of `-1`) per + system or team. Every further department has to be created below an + existing department. Department names have to be unique within the + same parent department. requestBody: content: application/json: schema: type: object + required: + - name + - parent_id properties: - group_name: - $ref: "#/components/schemas/system_admin_group_name" - parent_group: - $ref: "#/components/schemas/parent_group" + name: + $ref: "#/components/schemas/department_name" + parent_id: + $ref: "#/components/schemas/department_parent_id" + org_id: + $ref: "#/components/schemas/department_org_id" security: - AccountTokenAuth: [] responses: @@ -2880,36 +2885,122 @@ paths: schema: type: object properties: - id: - type: integer - name: - type: string - owner: - type: string - created_at: - type: string - parent_group_id: - type: integer - quota: - type: integer + department: + type: object + properties: + id: + type: integer + name: + type: string + parent_id: + type: integer + org_id: + type: integer + id_in_org: + type: integer example: - id: 2 - name: Test Department - owner: system admin - created_at: "2021-02-26T14:17:06+00:00" - parent_group_id: 1 - quota: -2 - /api/v2.1/admin/address-book/groups/{department_id}/: + department: + id: 2 + name: Developers + parent_id: 1 + org_id: -1 + id_in_org: 2 + "400": + description: Bad Request + content: + application/json: + schema: + type: object + example: + error_msg: Top department exists + /api/v2.1/admin/address-book-v2/departments/{department_id}/: + put: + tags: + - Departments + summary: Update Department + operationId: updateDepartment + description: >- + Rename a department by its ID. If the department has a + [group](/reference/createdepartmentgroup), the group is renamed as + well. + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + $ref: "#/components/schemas/department_name" + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/department_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + department: + type: object + properties: + id: + type: integer + name: + type: string + parent_id: + type: integer + org_id: + type: integer + id_in_org: + type: integer + example: + department: + id: 2 + name: Developers + parent_id: 1 + org_id: -1 + id_in_org: 2 + delete: + tags: + - Departments + summary: Delete Department + operationId: deleteDepartment + description: >- + Delete a department by its ID. The department's group (if any) is + deleted along with it. + + + > 🚧 Department must not have sub-departments + > + > A department can only be deleted if it has no sub-departments. + Delete all sub-departments first, otherwise the API returns `400` + with `"Forbidden deleting departments with sub departments"`. + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/department_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + example: + success: true + /api/v2.1/admin/address-book-v2/departments/{department_id}/members/: get: tags: - Departments - summary: Get Department - operationId: getDepartments - description: Get the information of a certain department by its ID. + summary: List Department Members + operationId: listDepartmentMembers + description: List the members of a department by its ID. security: - AccountTokenAuth: [] parameters: - - $ref: "#/components/parameters/return_ancestors" - $ref: "#/components/parameters/department_id" responses: "200": @@ -2919,52 +3010,266 @@ paths: schema: type: object properties: - id: - type: integer - name: - type: string - owner: - type: string - created_at: - type: string - parent_group_id: - type: integer - quota: - type: integer - groups: + member_list: type: array items: type: object - members: + example: + member_list: + - email: 167a5413f442440389eb69dcd01d45bf@auth.local + name: Jasmin Tee + contact_email: jasmin@example.com + avatar_url: https://cloud.seatable.io/media/avatars/default.png + is_staff: false + post: + tags: + - Departments + summary: Add Department Members + operationId: addDepartmentMembers + description: >- + Add one or more users to a department. Repeat the `email` field for + every user you want to add. + requestBody: + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - email + properties: + email: + type: array + items: + $ref: "#/components/schemas/username" + encoding: + email: + style: form + explode: true + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/department_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + success: type: array items: type: object - ancestor_groups: + failed: type: array items: type: object example: - id: 2 - name: Test department - owner: system admin - created_at: "2021-02-26T11:21:34+00:00" - parent_group_id: 74 - quota: -2 - groups: [] - members: [] - ancestor_groups: - - id: 1 - name: sys-dep-1 - owner: system admin - created_at: "2021-02-26T11:20:47+00:00" - parent_group_id: -1 - quota: -2 + success: + - email: 167a5413f442440389eb69dcd01d45bf@auth.local + name: Jasmin Tee + avatar_url: https://cloud.seatable.io/media/avatars/default.png + contact_email: jasmin@example.com + failed: + - email: 12abc456def789abc123def456abc789@auth.local + error_msg: User Max Example is already a department member. + /api/v2.1/admin/address-book-v2/departments/{department_id}/members/{user_id}/: + put: + tags: + - Departments + summary: Update Department Member + operationId: updateDepartmentMember + description: Set whether a department member is an administrator of the department. + requestBody: + content: + application/json: + schema: + type: object + properties: + is_staff: + $ref: "#/components/schemas/department_is_staff" + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/department_id" + - $ref: "#/components/parameters/user_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + member: + type: object + properties: + email: + type: string + name: + type: string + avatar_url: + type: string + contact_email: + type: string + is_staff: + type: boolean + example: + member: + email: 167a5413f442440389eb69dcd01d45bf@auth.local + name: Jasmin Tee + avatar_url: https://cloud.seatable.io/media/avatars/default.png + contact_email: jasmin@example.com + is_staff: true delete: tags: - Departments - summary: Delete Department - operationId: deleteDepartment - description: Delete a department by its ID. + summary: Remove Department Member + operationId: removeDepartmentMember + description: Remove a user from a department. + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/department_id" + - $ref: "#/components/parameters/user_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + example: + success: true + /api/v2.1/admin/address-book-v2/departments/add-to-departments/: + post: + tags: + - Departments + summary: Add User to Departments + operationId: addUserToDepartments + description: >- + Add a single user to one or more system-level departments at once. + Departments the user is already a member of are listed under `failed`. + requestBody: + content: + application/json: + schema: + type: object + required: + - email + - department_ids + properties: + email: + $ref: "#/components/schemas/username" + department_ids: + $ref: "#/components/schemas/department_ids" + security: + - AccountTokenAuth: [] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + success: + type: array + items: + type: object + failed: + type: array + items: + type: object + example: + success: + - id: 3 + name: Test Department + parent_id: 1 + org_id: -1 + id_in_org: 3 + failed: + - department: + id: 2 + name: Developers + parent_id: 1 + org_id: -1 + id_in_org: 2 + error_msg: User has been in Developers + /api/v2.1/admin/address-book-v2/departments/{department_id}/group/: + post: + tags: + - Departments + summary: Create Department Group + operationId: createDepartmentGroup + description: >- + Create a group (with its own workspace) for a department, so that the + department can own bases. A department can have at most one group. + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/department_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + group_id: + type: integer + group_name: + type: string + example: + group_id: 48 + group_name: Developers + "400": + description: Bad Request + content: + application/json: + schema: + type: object + example: + error_msg: Group of department exists + get: + tags: + - Departments + summary: Get Department Group + operationId: getDepartmentGroup + description: Get the group of a department. Returns `404` if the department has no group. + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/department_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + group_id: + type: integer + group_name: + type: string + example: + group_id: 48 + group_name: Developers + delete: + tags: + - Departments + summary: Delete Department Group + operationId: deleteDepartmentGroup + description: >- + Delete the group of a department. The department itself is kept. + + + > 🚧 Group must be empty + > + > The group can only be deleted if it contains no bases. Delete or move all bases out of the group first, otherwise the API returns `400` with `"Cannot delete group with bases"`. security: - AccountTokenAuth: [] parameters: @@ -2978,6 +3283,35 @@ paths: type: object example: success: true + /api/v2.1/admin/address-book-v2/non-department-users/: + get: + tags: + - Departments + summary: List Users without Department + operationId: listNonDepartmentUsers + description: >- + List all active users that are not a member of any department. Team + users are not included. + security: + - AccountTokenAuth: [] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + user_list: + type: array + items: + type: object + example: + user_list: + - email: 12abc456def789abc123def456abc789@auth.local + name: Max Example + contact_email: max@example.com + avatar_url: https://cloud.seatable.io/media/avatars/default.png # Sharing Links /api/v2.1/admin/dtable/{base_id}/external-links/: diff --git a/tests/__snapshots__/test_departments/test_addDepartment.json b/tests/__snapshots__/test_departments/test_addDepartment.json new file mode 100644 index 0000000..8915f9e --- /dev/null +++ b/tests/__snapshots__/test_departments/test_addDepartment.json @@ -0,0 +1,9 @@ +{ + "department": { + "id": "int", + "id_in_org": "int", + "name": "Developers", + "org_id": -1, + "parent_id": "int" + } +} diff --git a/tests/__snapshots__/test_departments/test_addUserToDepartments.json b/tests/__snapshots__/test_departments/test_addUserToDepartments.json new file mode 100644 index 0000000..5daf56a --- /dev/null +++ b/tests/__snapshots__/test_departments/test_addUserToDepartments.json @@ -0,0 +1,23 @@ +{ + "failed": [ + { + "department": { + "id": "int", + "id_in_org": "int", + "name": "multi-add-1", + "org_id": -1, + "parent_id": "int" + }, + "error_msg": "User has been in multi-add-1" + } + ], + "success": [ + { + "id": "int", + "id_in_org": "int", + "name": "multi-add-2", + "org_id": -1, + "parent_id": "int" + } + ] +} diff --git a/tests/__snapshots__/test_departments/test_deleteDepartment.json b/tests/__snapshots__/test_departments/test_deleteDepartment.json new file mode 100644 index 0000000..5550c6d --- /dev/null +++ b/tests/__snapshots__/test_departments/test_deleteDepartment.json @@ -0,0 +1,3 @@ +{ + "success": true +} diff --git a/tests/__snapshots__/test_departments/test_department_group[create].json b/tests/__snapshots__/test_departments/test_department_group[create].json new file mode 100644 index 0000000..5401f56 --- /dev/null +++ b/tests/__snapshots__/test_departments/test_department_group[create].json @@ -0,0 +1,4 @@ +{ + "group_id": "int", + "group_name": "group-test" +} diff --git a/tests/__snapshots__/test_departments/test_department_group[delete].json b/tests/__snapshots__/test_departments/test_department_group[delete].json new file mode 100644 index 0000000..5550c6d --- /dev/null +++ b/tests/__snapshots__/test_departments/test_department_group[delete].json @@ -0,0 +1,3 @@ +{ + "success": true +} diff --git a/tests/__snapshots__/test_departments/test_department_members[add].json b/tests/__snapshots__/test_departments/test_department_members[add].json new file mode 100644 index 0000000..ddbb951 --- /dev/null +++ b/tests/__snapshots__/test_departments/test_department_members[add].json @@ -0,0 +1,11 @@ +{ + "failed": [], + "success": [ + { + "avatar_url": "str", + "contact_email": "testuser@example.com", + "email": "str", + "name": "Test User" + } + ] +} diff --git a/tests/__snapshots__/test_departments/test_department_members[list].json b/tests/__snapshots__/test_departments/test_department_members[list].json new file mode 100644 index 0000000..dd21a15 --- /dev/null +++ b/tests/__snapshots__/test_departments/test_department_members[list].json @@ -0,0 +1,11 @@ +{ + "member_list": [ + { + "avatar_url": "str", + "contact_email": "testuser@example.com", + "email": "str", + "is_staff": false, + "name": "Test User" + } + ] +} diff --git a/tests/__snapshots__/test_departments/test_department_members[remove].json b/tests/__snapshots__/test_departments/test_department_members[remove].json new file mode 100644 index 0000000..5550c6d --- /dev/null +++ b/tests/__snapshots__/test_departments/test_department_members[remove].json @@ -0,0 +1,3 @@ +{ + "success": true +} diff --git a/tests/__snapshots__/test_departments/test_department_members[update].json b/tests/__snapshots__/test_departments/test_department_members[update].json new file mode 100644 index 0000000..760c89d --- /dev/null +++ b/tests/__snapshots__/test_departments/test_department_members[update].json @@ -0,0 +1,9 @@ +{ + "member": { + "avatar_url": "str", + "contact_email": "testuser@example.com", + "email": "str", + "is_staff": true, + "name": "Test User" + } +} diff --git a/tests/__snapshots__/test_departments/test_listDepartments_top_level.json b/tests/__snapshots__/test_departments/test_listDepartments_top_level.json new file mode 100644 index 0000000..1a8b266 --- /dev/null +++ b/tests/__snapshots__/test_departments/test_listDepartments_top_level.json @@ -0,0 +1,11 @@ +{ + "department_list": [ + { + "id": "int", + "id_in_org": "int", + "name": "automated-testing-top", + "org_id": -1, + "parent_id": "int" + } + ] +} diff --git a/tests/__snapshots__/test_departments/test_listNonDepartmentUsers.json b/tests/__snapshots__/test_departments/test_listNonDepartmentUsers.json new file mode 100644 index 0000000..01a708f --- /dev/null +++ b/tests/__snapshots__/test_departments/test_listNonDepartmentUsers.json @@ -0,0 +1,6 @@ +{ + "avatar_url": "str", + "contact_email": "testuser@example.com", + "email": "str", + "name": "Test User" +} diff --git a/tests/__snapshots__/test_departments/test_updateDepartment.json b/tests/__snapshots__/test_departments/test_updateDepartment.json new file mode 100644 index 0000000..50811ad --- /dev/null +++ b/tests/__snapshots__/test_departments/test_updateDepartment.json @@ -0,0 +1,9 @@ +{ + "department": { + "id": "int", + "id_in_org": "int", + "name": "new-name", + "org_id": -1, + "parent_id": "int" + } +} diff --git a/tests/__snapshots__/test_user_departments/test_department_members[add].json b/tests/__snapshots__/test_user_departments/test_department_members[add].json new file mode 100644 index 0000000..6532954 --- /dev/null +++ b/tests/__snapshots__/test_user_departments/test_department_members[add].json @@ -0,0 +1,16 @@ +{ + "failed": [], + "success": [ + { + "avatar_url": "str", + "contact_email": "admin@example.com", + "department_id": "int", + "email": "str", + "group_id": "int", + "is_admin": false, + "login_id": "", + "name": "admin", + "role": "Member" + } + ] +} diff --git a/tests/__snapshots__/test_user_departments/test_department_members[remove].json b/tests/__snapshots__/test_user_departments/test_department_members[remove].json new file mode 100644 index 0000000..5550c6d --- /dev/null +++ b/tests/__snapshots__/test_user_departments/test_department_members[remove].json @@ -0,0 +1,3 @@ +{ + "success": true +} diff --git a/tests/__snapshots__/test_user_departments/test_department_members[update].json b/tests/__snapshots__/test_user_departments/test_department_members[update].json new file mode 100644 index 0000000..a8bea55 --- /dev/null +++ b/tests/__snapshots__/test_user_departments/test_department_members[update].json @@ -0,0 +1,11 @@ +{ + "avatar_url": "str", + "contact_email": "admin@example.com", + "department_id": "int", + "email": "str", + "group_id": "int", + "is_admin": true, + "login_id": "", + "name": "admin", + "role": "Admin" +} diff --git a/tests/__snapshots__/test_user_departments/test_getDepartmentGroupMembersCount.json b/tests/__snapshots__/test_user_departments/test_getDepartmentGroupMembersCount.json new file mode 100644 index 0000000..400decb --- /dev/null +++ b/tests/__snapshots__/test_user_departments/test_getDepartmentGroupMembersCount.json @@ -0,0 +1,3 @@ +{ + "count": 1 +} diff --git a/tests/__snapshots__/test_user_departments/test_listDepartmentMemberBases.json b/tests/__snapshots__/test_user_departments/test_listDepartmentMemberBases.json new file mode 100644 index 0000000..8cf902f --- /dev/null +++ b/tests/__snapshots__/test_user_departments/test_listDepartmentMemberBases.json @@ -0,0 +1,18 @@ +{ + "dtable_list": [ + { + "backend": "js", + "color": null, + "created_at": "str", + "icon": null, + "id": "int", + "in_storage": true, + "is_encrypted": false, + "name": "automated-testing-departments", + "text_color": null, + "updated_at": "str", + "uuid": "str", + "workspace_id": "int" + } + ] +} diff --git a/tests/__snapshots__/test_user_departments/test_listDepartmentMembers.json b/tests/__snapshots__/test_user_departments/test_listDepartmentMembers.json new file mode 100644 index 0000000..245f740 --- /dev/null +++ b/tests/__snapshots__/test_user_departments/test_listDepartmentMembers.json @@ -0,0 +1,13 @@ +{ + "member_list": [ + { + "avatar_url": "str", + "contact_email": "testuser@example.com", + "email": "str", + "is_admin": true, + "is_staff": true, + "name": "Test User", + "role": "Admin" + } + ] +} diff --git a/tests/__snapshots__/test_user_departments/test_listDepartments.json b/tests/__snapshots__/test_user_departments/test_listDepartments.json new file mode 100644 index 0000000..ef4e699 --- /dev/null +++ b/tests/__snapshots__/test_user_departments/test_listDepartments.json @@ -0,0 +1,18 @@ +{ + "departments": [ + { + "id": "int", + "id_in_org": "int", + "name": "automated-testing-top", + "org_id": -1, + "parent_id": "int" + }, + { + "id": "int", + "id_in_org": "int", + "name": "automated-testing-sub", + "org_id": -1, + "parent_id": "int" + } + ] +} diff --git a/tests/__snapshots__/test_user_departments/test_listSubDepartments.json b/tests/__snapshots__/test_user_departments/test_listSubDepartments.json new file mode 100644 index 0000000..4bd0b59 --- /dev/null +++ b/tests/__snapshots__/test_user_departments/test_listSubDepartments.json @@ -0,0 +1,11 @@ +{ + "department_list": [ + { + "id": "int", + "id_in_org": "int", + "name": "automated-testing-sub", + "org_id": -1, + "parent_id": "int" + } + ] +} diff --git a/tests/__snapshots__/test_user_departments/test_listUserDepartments.json b/tests/__snapshots__/test_user_departments/test_listUserDepartments.json new file mode 100644 index 0000000..e9127ac --- /dev/null +++ b/tests/__snapshots__/test_user_departments/test_listUserDepartments.json @@ -0,0 +1,12 @@ +{ + "department_list": [ + { + "id": "int", + "id_in_org": "int", + "name": "automated-testing-sub", + "org_id": -1, + "parent_id": "int", + "sub_departments": [] + } + ] +} diff --git a/tests/conftest.py b/tests/conftest.py index dc41a24..1eb24b0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -356,6 +356,69 @@ def delete_group(account_token: Secret, group_id: int): assert response.status_code == 200 +# Departments (system admin) + +def _admin_headers(token: Secret) -> dict: + return {'Authorization': f'Bearer {token.value}'} + + +def list_departments(token: Secret, parent_id: int = -1) -> list[dict]: + case: Case = system_admin_account_operations.find_operation_by_id('listDepartments') \ + .Case(query={'parent_id': parent_id}) + response = case.call(headers=_admin_headers(token)) + assert response.status_code == 200 + return response.json()['department_list'] + + +def create_department(token: Secret, name: str, parent_id: int) -> int: + case: Case = system_admin_account_operations.find_operation_by_id('addDepartment') \ + .Case(body={'name': name, 'parent_id': parent_id}) + response = case.call(headers=_admin_headers(token)) + assert response.status_code == 200 + + department_id = response.json()['department']['id'] + assert isinstance(department_id, int) + return department_id + + +def delete_department(token: Secret, department_id: int): + """Deletes a department including all of its sub-departments.""" + if CLEANUP_AFTER_TESTS != 'True': + return + + for sub_department in list_departments(token, department_id): + delete_department(token, sub_department['id']) + + case: Case = system_admin_account_operations.find_operation_by_id('deleteDepartment') \ + .Case(path_parameters={'department_id': department_id}) + response = case.call(headers=_admin_headers(token)) + assert response.status_code == 200 + + +@pytest.fixture(scope='module') +def top_department(system_admin_account_token: Secret) -> Generator[int, None, None]: + """There can be only one top-level department, so it is shared by all tests in a module.""" + existing = list_departments(system_admin_account_token) + if existing: + department_id = existing[0]['id'] + else: + department_id = create_department(system_admin_account_token, 'automated-testing-top', -1) + + yield department_id + + delete_department(system_admin_account_token, department_id) + + +@pytest.fixture(scope='module') +def user_id(system_admin_account_token: Secret) -> str: + """The internal @auth.local ID of the regular test user.""" + case: Case = system_admin_account_operations.find_operation_by_id('listUsers').Case() + response = case.call(headers=_admin_headers(system_admin_account_token)) + assert response.status_code == 200 + + user = next(u for u in response.json()['data'] if u['contact_email'] == USERNAME) + return user['email'] + MIN_LICENSE_USERS = 10 def _get_license_maxusers() -> int: diff --git a/tests/test_departments.py b/tests/test_departments.py new file mode 100644 index 0000000..aba4d5b --- /dev/null +++ b/tests/test_departments.py @@ -0,0 +1,298 @@ +import pytest +from conftest import ( + Secret, system_admin_account_operations, + create_department, delete_department, list_departments, +) +from schemathesis import Case +from syrupy.assertion import SnapshotAssertion +from syrupy.matchers import path_type + +pytestmark = pytest.mark.needs_large_license + +# IDs are auto-incremented and differ between runs +DEPARTMENT_MATCHER = path_type({ + r'(.*\.)?id': (int,), + r'(.*\.)?parent_id': (int,), + r'(.*\.)?id_in_org': (int,), +}, regex=True) + +MEMBER_MATCHER = path_type({ + r'(.*\.)?email': (str,), + r'(.*\.)?avatar_url': (str,), +}, regex=True) + + +def test_listDepartments_top_level(system_admin_account_token: Secret, top_department: int, snapshot_json: SnapshotAssertion): + case: Case = system_admin_account_operations.find_operation_by_id('listDepartments').Case() + response = case.call(headers={'Authorization': f'Bearer {system_admin_account_token.value}'}) + + assert response.status_code == 200 + + data = response.json() + assert [d['id'] for d in data['department_list']] == [top_department] + + assert snapshot_json(matcher=DEPARTMENT_MATCHER) == data + + +def test_addDepartment(system_admin_account_token: Secret, top_department: int, snapshot_json: SnapshotAssertion): + headers = {'Authorization': f'Bearer {system_admin_account_token.value}'} + + body = {'name': 'Developers', 'parent_id': top_department} + case: Case = system_admin_account_operations.find_operation_by_id('addDepartment').Case(body=body) + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + department_id = data['department']['id'] + + try: + assert data['department']['name'] == 'Developers' + assert data['department']['parent_id'] == top_department + assert data['department']['org_id'] == -1 + + assert snapshot_json(matcher=DEPARTMENT_MATCHER) == data + + # Verify the department shows up below its parent + sub_departments = list_departments(system_admin_account_token, top_department) + assert [d['id'] for d in sub_departments] == [department_id] + + finally: + delete_department(system_admin_account_token, department_id) + + +def test_addDepartment_second_top_level(system_admin_account_token: Secret, top_department: int): + """Only one top-level department is allowed.""" + body = {'name': 'another-top-level', 'parent_id': -1} + case: Case = system_admin_account_operations.find_operation_by_id('addDepartment').Case(body=body) + response = case.call(headers={'Authorization': f'Bearer {system_admin_account_token.value}'}) + + assert response.status_code == 400 + assert response.json()['error_msg'] == 'Top department exists' + + +def test_updateDepartment(system_admin_account_token: Secret, top_department: int, snapshot_json: SnapshotAssertion): + department_id = create_department(system_admin_account_token, 'old-name', top_department) + + try: + case: Case = system_admin_account_operations.find_operation_by_id('updateDepartment') \ + .Case(path_parameters={'department_id': department_id}, body={'name': 'new-name'}) + response = case.call(headers={'Authorization': f'Bearer {system_admin_account_token.value}'}) + + assert response.status_code == 200 + + data = response.json() + assert data['department']['id'] == department_id + assert data['department']['name'] == 'new-name' + + assert snapshot_json(matcher=DEPARTMENT_MATCHER) == data + + sub_departments = list_departments(system_admin_account_token, top_department) + assert next(d for d in sub_departments if d['id'] == department_id)['name'] == 'new-name' + + finally: + delete_department(system_admin_account_token, department_id) + + +def test_deleteDepartment(system_admin_account_token: Secret, top_department: int, snapshot_json: SnapshotAssertion): + department_id = create_department(system_admin_account_token, 'to-be-deleted', top_department) + + case: Case = system_admin_account_operations.find_operation_by_id('deleteDepartment') \ + .Case(path_parameters={'department_id': department_id}) + response = case.call(headers={'Authorization': f'Bearer {system_admin_account_token.value}'}) + + assert response.status_code == 200 + assert snapshot_json == response.json() + + sub_departments = list_departments(system_admin_account_token, top_department) + assert department_id not in [d['id'] for d in sub_departments] + + +def test_department_members(system_admin_account_token: Secret, top_department: int, user_id: str, snapshot_json: SnapshotAssertion): + """Add a member, list members, promote the member to department admin, remove the member.""" + headers = {'Authorization': f'Bearer {system_admin_account_token.value}'} + department_id = create_department(system_admin_account_token, 'members-test', top_department) + path_parameters = {'department_id': department_id} + member_path_parameters = {'department_id': department_id, 'user_id': user_id} + + try: + # Add member + case: Case = system_admin_account_operations.find_operation_by_id('addDepartmentMembers') \ + .Case(path_parameters=path_parameters, body={'email': [user_id]}) + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + assert data['failed'] == [] + assert [m['email'] for m in data['success']] == [user_id] + assert snapshot_json(name='add', matcher=MEMBER_MATCHER) == data + + # Adding the same member again fails + response = case.call(headers=headers) + + assert response.status_code == 200 + assert response.json()['success'] == [] + assert [m['email'] for m in response.json()['failed']] == [user_id] + + # List members + case = system_admin_account_operations.find_operation_by_id('listDepartmentMembers') \ + .Case(path_parameters=path_parameters) + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + assert [m['email'] for m in data['member_list']] == [user_id] + assert data['member_list'][0]['is_staff'] is False + assert snapshot_json(name='list', matcher=MEMBER_MATCHER) == data + + # Promote member to department admin + case = system_admin_account_operations.find_operation_by_id('updateDepartmentMember') \ + .Case(path_parameters=member_path_parameters, body={'is_staff': True}) + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + assert data['member']['email'] == user_id + assert data['member']['is_staff'] is True + assert snapshot_json(name='update', matcher=MEMBER_MATCHER) == data + + # Remove member + case = system_admin_account_operations.find_operation_by_id('removeDepartmentMember') \ + .Case(path_parameters=member_path_parameters) + response = case.call(headers=headers) + + assert response.status_code == 200 + assert snapshot_json(name='remove') == response.json() + + case = system_admin_account_operations.find_operation_by_id('listDepartmentMembers') \ + .Case(path_parameters=path_parameters) + response = case.call(headers=headers) + + assert response.status_code == 200 + assert response.json()['member_list'] == [] + + finally: + delete_department(system_admin_account_token, department_id) + + +def test_addUserToDepartments(system_admin_account_token: Secret, top_department: int, user_id: str, snapshot_json: SnapshotAssertion): + headers = {'Authorization': f'Bearer {system_admin_account_token.value}'} + first_id = create_department(system_admin_account_token, 'multi-add-1', top_department) + second_id = create_department(system_admin_account_token, 'multi-add-2', top_department) + + try: + # User is already a member of the first department + case: Case = system_admin_account_operations.find_operation_by_id('addDepartmentMembers') \ + .Case(path_parameters={'department_id': first_id}, body={'email': [user_id]}) + response = case.call(headers=headers) + assert response.status_code == 200 + + body = {'email': user_id, 'department_ids': [first_id, second_id]} + case = system_admin_account_operations.find_operation_by_id('addUserToDepartments').Case(body=body) + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + assert [d['id'] for d in data['success']] == [second_id] + assert [f['department']['id'] for f in data['failed']] == [first_id] + assert snapshot_json(matcher=DEPARTMENT_MATCHER) == data + + finally: + delete_department(system_admin_account_token, first_id) + delete_department(system_admin_account_token, second_id) + + +def test_department_group(system_admin_account_token: Secret, top_department: int, snapshot_json: SnapshotAssertion): + """Create the group of a department, fetch it, delete it.""" + headers = {'Authorization': f'Bearer {system_admin_account_token.value}'} + department_id = create_department(system_admin_account_token, 'group-test', top_department) + path_parameters = {'department_id': department_id} + group_matcher = path_type({'group_id': (int,)}) + + try: + # Create group + case: Case = system_admin_account_operations.find_operation_by_id('createDepartmentGroup') \ + .Case(path_parameters=path_parameters) + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + group_id = data['group_id'] + assert isinstance(group_id, int) + assert data['group_name'] == 'group-test' + assert snapshot_json(name='create', matcher=group_matcher) == data + + # A department can have only one group + response = case.call(headers=headers) + + assert response.status_code == 400 + assert response.json()['error_msg'] == 'Group of department exists' + + # Get group + case = system_admin_account_operations.find_operation_by_id('getDepartmentGroup') \ + .Case(path_parameters=path_parameters) + response = case.call(headers=headers) + + assert response.status_code == 200 + assert response.json() == {'group_id': group_id, 'group_name': 'group-test'} + + # Renaming the department renames the group + case = system_admin_account_operations.find_operation_by_id('updateDepartment') \ + .Case(path_parameters=path_parameters, body={'name': 'group-test-renamed'}) + response = case.call(headers=headers) + assert response.status_code == 200 + + case = system_admin_account_operations.find_operation_by_id('getDepartmentGroup') \ + .Case(path_parameters=path_parameters) + response = case.call(headers=headers) + + assert response.status_code == 200 + assert response.json() == {'group_id': group_id, 'group_name': 'group-test-renamed'} + + # Delete group + case = system_admin_account_operations.find_operation_by_id('deleteDepartmentGroup') \ + .Case(path_parameters=path_parameters) + response = case.call(headers=headers) + + assert response.status_code == 200 + assert snapshot_json(name='delete') == response.json() + + finally: + delete_department(system_admin_account_token, department_id) + + +def test_listNonDepartmentUsers(system_admin_account_token: Secret, top_department: int, user_id: str, snapshot_json: SnapshotAssertion): + headers = {'Authorization': f'Bearer {system_admin_account_token.value}'} + department_id = create_department(system_admin_account_token, 'non-dep-test', top_department) + + try: + case: Case = system_admin_account_operations.find_operation_by_id('listNonDepartmentUsers').Case() + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + assert user_id in [u['email'] for u in data['user_list']] + + user = next(u for u in data['user_list'] if u['email'] == user_id) + assert snapshot_json(matcher=MEMBER_MATCHER) == user + + # Once the user is a department member, they are no longer listed + case = system_admin_account_operations.find_operation_by_id('addDepartmentMembers') \ + .Case(path_parameters={'department_id': department_id}, body={'email': [user_id]}) + response = case.call(headers=headers) + assert response.status_code == 200 + + case = system_admin_account_operations.find_operation_by_id('listNonDepartmentUsers').Case() + response = case.call(headers=headers) + + assert response.status_code == 200 + assert user_id not in [u['email'] for u in response.json()['user_list']] + + finally: + delete_department(system_admin_account_token, department_id) diff --git a/tests/test_user_departments.py b/tests/test_user_departments.py new file mode 100644 index 0000000..dc323e7 --- /dev/null +++ b/tests/test_user_departments.py @@ -0,0 +1,257 @@ +import pytest +from conftest import ( + Secret, system_admin_account_operations, user_account_operations, ADMIN_USERNAME, + create_department, delete_department, +) +from dataclasses import dataclass +from schemathesis import Case +from syrupy.assertion import SnapshotAssertion +from syrupy.matchers import path_type +from typing import Generator + +pytestmark = pytest.mark.needs_large_license + +# IDs are auto-incremented and differ between runs +DEPARTMENT_MATCHER = path_type({ + r'(.*\.)?id': (int,), + r'(.*\.)?parent_id': (int,), + r'(.*\.)?id_in_org': (int,), +}, regex=True) + +MEMBER_MATCHER = path_type({ + r'(.*\.)?email': (str,), + r'(.*\.)?avatar_url': (str,), + r'(.*\.)?group_id': (int,), + r'(.*\.)?department_id': (int,), +}, regex=True) + + +@dataclass +class Department: + id: int + group_id: int + + +@pytest.fixture +def department(system_admin_account_token: Secret, top_department: int, user_id: str) -> Generator[Department, None, None]: + """A sub-department with a group, of which the regular test user is a department admin.""" + headers = {'Authorization': f'Bearer {system_admin_account_token.value}'} + department_id = create_department(system_admin_account_token, 'automated-testing-sub', top_department) + path_parameters = {'department_id': department_id} + + case: Case = system_admin_account_operations.find_operation_by_id('createDepartmentGroup') \ + .Case(path_parameters=path_parameters) + response = case.call(headers=headers) + assert response.status_code == 200 + group_id = response.json()['group_id'] + + case = system_admin_account_operations.find_operation_by_id('addDepartmentMembers') \ + .Case(path_parameters=path_parameters, body={'email': [user_id]}) + response = case.call(headers=headers) + assert response.status_code == 200 + + case = system_admin_account_operations.find_operation_by_id('updateDepartmentMember') \ + .Case(path_parameters={'department_id': department_id, 'user_id': user_id}, body={'is_staff': True}) + response = case.call(headers=headers) + assert response.status_code == 200 + + yield Department(id=department_id, group_id=group_id) + + delete_department(system_admin_account_token, department_id) + + +@pytest.fixture(scope='module') +def admin_user_id(system_admin_account_token: Secret) -> str: + """The internal @auth.local ID of the system admin.""" + case: Case = system_admin_account_operations.find_operation_by_id('listUsers').Case() + response = case.call(headers={'Authorization': f'Bearer {system_admin_account_token.value}'}) + assert response.status_code == 200 + + user = next(u for u in response.json()['data'] if u['contact_email'] == ADMIN_USERNAME) + return user['email'] + + +def test_listDepartments(account_token: Secret, top_department: int, department: Department, snapshot_json: SnapshotAssertion): + case: Case = user_account_operations.find_operation_by_id('listDepartments').Case() + response = case.call(headers={'Authorization': f'Bearer {account_token.value}'}) + + assert response.status_code == 200 + + data = response.json() + assert [d['id'] for d in data['departments']] == [top_department, department.id] + + assert snapshot_json(matcher=DEPARTMENT_MATCHER) == data + + +def test_listUserDepartments(account_token: Secret, department: Department, snapshot_json: SnapshotAssertion): + case: Case = user_account_operations.find_operation_by_id('listUserDepartments').Case() + response = case.call(headers={'Authorization': f'Bearer {account_token.value}'}) + + assert response.status_code == 200 + + data = response.json() + assert [d['id'] for d in data['department_list']] == [department.id] + assert data['department_list'][0]['sub_departments'] == [] + + assert snapshot_json(matcher=DEPARTMENT_MATCHER) == data + + +def test_listSubDepartments(account_token: Secret, top_department: int, department: Department, snapshot_json: SnapshotAssertion): + case: Case = user_account_operations.find_operation_by_id('listSubDepartments') \ + .Case(path_parameters={'department_id': top_department}) + response = case.call(headers={'Authorization': f'Bearer {account_token.value}'}) + + assert response.status_code == 200 + + data = response.json() + assert [d['id'] for d in data['department_list']] == [department.id] + + assert snapshot_json(matcher=DEPARTMENT_MATCHER) == data + + +def test_listDepartmentMembers(account_token: Secret, department: Department, user_id: str, snapshot_json: SnapshotAssertion): + case: Case = user_account_operations.find_operation_by_id('listDepartmentMembers') \ + .Case(path_parameters={'department_id': department.id}) + response = case.call(headers={'Authorization': f'Bearer {account_token.value}'}) + + assert response.status_code == 200 + + data = response.json() + assert [m['email'] for m in data['member_list']] == [user_id] + assert data['member_list'][0]['is_admin'] is True + assert data['member_list'][0]['role'] == 'Admin' + + assert snapshot_json(matcher=MEMBER_MATCHER) == data + + +def test_department_members(account_token: Secret, department: Department, admin_user_id: str, snapshot_json: SnapshotAssertion): + """As department admin: add a member, promote them to department admin, remove them.""" + headers = {'Authorization': f'Bearer {account_token.value}'} + path_parameters = {'department_id': department.id} + member_path_parameters = {'department_id': department.id, 'user_id': admin_user_id} + + # Add member + case: Case = user_account_operations.find_operation_by_id('addDepartmentMembers') \ + .Case(path_parameters=path_parameters, body={'emails': admin_user_id}) + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + assert data['failed'] == [] + assert [m['email'] for m in data['success']] == [admin_user_id] + assert data['success'][0]['role'] == 'Member' + assert snapshot_json(name='add', matcher=MEMBER_MATCHER) == data + + # Adding the same member again fails + response = case.call(headers=headers) + + assert response.status_code == 200 + assert response.json()['success'] == [] + assert [m['email'] for m in response.json()['failed']] == [admin_user_id] + + # Promote member to department admin + case = user_account_operations.find_operation_by_id('updateDepartmentMember') \ + .Case(path_parameters=member_path_parameters, body={'is_admin': 'true'}) + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + assert data['email'] == admin_user_id + assert data['is_admin'] is True + assert data['role'] == 'Admin' + assert snapshot_json(name='update', matcher=MEMBER_MATCHER) == data + + # Remove member + case = user_account_operations.find_operation_by_id('removeDepartmentMember') \ + .Case(path_parameters=member_path_parameters) + response = case.call(headers=headers) + + assert response.status_code == 200 + assert snapshot_json(name='remove') == response.json() + + case = user_account_operations.find_operation_by_id('listDepartmentMembers') \ + .Case(path_parameters=path_parameters) + response = case.call(headers=headers) + + assert response.status_code == 200 + assert admin_user_id not in [m['email'] for m in response.json()['member_list']] + + +def test_listDepartmentMemberBases(account_token: Secret, system_admin_account_token: Secret, top_department: int, department: Department, user_id: str, snapshot_json: SnapshotAssertion): + """Members of an ancestor department can list the personal bases of a department member.""" + headers = {'Authorization': f'Bearer {account_token.value}'} + + # The test user has to be a member of an ancestor department (the top-level department) of `department` + case: Case = system_admin_account_operations.find_operation_by_id('addDepartmentMembers') \ + .Case(path_parameters={'department_id': top_department}, body={'email': [user_id]}) + response = case.call(headers={'Authorization': f'Bearer {system_admin_account_token.value}'}) + assert response.status_code == 200 + + # Create a base in the personal workspace of the test user + case = user_account_operations.find_operation_by_id('listWorkspaces').Case() + response = case.call(headers=headers) + assert response.status_code == 200 + workspace_id = next(w['id'] for w in response.json()['workspace_list'] if w['type'] == 'personal') + + base_name = 'automated-testing-departments' + case = user_account_operations.find_operation_by_id('createBase') \ + .Case(body={'workspace_id': workspace_id, 'name': base_name}) + response = case.call(headers=headers) + assert response.status_code == 201 + base_uuid = response.json()['table']['uuid'] + + try: + case = user_account_operations.find_operation_by_id('listDepartmentMemberBases') \ + .Case(path_parameters={'department_id': department.id, 'user_id': user_id}) + response = case.call(headers=headers) + + assert response.status_code == 200 + + data = response.json() + assert [b['uuid'] for b in data['dtable_list']] == [base_uuid] + + matcher = path_type({ + r'dtable_list\..*\.id': (int,), + r'dtable_list\..*\.workspace_id': (int,), + r'dtable_list\..*\.uuid': (str,), + r'dtable_list\..*\.created_at': (str,), + r'dtable_list\..*\.updated_at': (str,), + }, regex=True) + assert snapshot_json(matcher=matcher) == data + + finally: + case = user_account_operations.find_operation_by_id('deleteBase') \ + .Case(path_parameters={'workspace_id': workspace_id}, body={'name': base_name}) + response = case.call(headers=headers) + assert response.status_code == 200 + + case = system_admin_account_operations.find_operation_by_id('removeDepartmentMember') \ + .Case(path_parameters={'department_id': top_department, 'user_id': user_id}) + response = case.call(headers={'Authorization': f'Bearer {system_admin_account_token.value}'}) + assert response.status_code == 200 + + +def test_getDepartmentGroupMembersCount(account_token: Secret, department: Department, admin_user_id: str, snapshot_json: SnapshotAssertion): + headers = {'Authorization': f'Bearer {account_token.value}'} + + case: Case = user_account_operations.find_operation_by_id('getDepartmentGroupMembersCount') \ + .Case(path_parameters={'group_id': department.group_id}) + response = case.call(headers=headers) + + assert response.status_code == 200 + assert snapshot_json == response.json() + + # Adding a member increases the count + case = user_account_operations.find_operation_by_id('addDepartmentMembers') \ + .Case(path_parameters={'department_id': department.id}, body={'emails': admin_user_id}) + response = case.call(headers=headers) + assert response.status_code == 200 + + case = user_account_operations.find_operation_by_id('getDepartmentGroupMembersCount') \ + .Case(path_parameters={'group_id': department.group_id}) + response = case.call(headers=headers) + + assert response.status_code == 200 + assert response.json()['count'] == 2 diff --git a/user_account_operations.yaml b/user_account_operations.yaml index ee4e22f..e6ddb01 100644 --- a/user_account_operations.yaml +++ b/user_account_operations.yaml @@ -634,6 +634,10 @@ components: is_admin: type: string enum: ["", "true", "false"] + department_member_emails: + type: string + description: The unique user IDs (`xxx@auth.local`) of the users to add, separated by commas. + example: 12abc456def789abc123def456abc789@auth.local,2abc456def789abc123def456abc789a@auth.local is_inactive: type: object properties: @@ -6294,20 +6298,20 @@ paths: last_sync_time: "2021-08-26T12:30:44+00:00" # Departments - /api/v2.1/address-book/departments/: + /api/v2.1/address-book-v2/departments/: get: tags: - Departments summary: List Departments operationId: listDepartments description: >- - As a team (organization) user, you can use this API request to list all - the departments in your team (organization). + List all the departments of the system or, as a team user, of your team + (organization). The returned `id` values are the IDs of each department. If the - `parent_group_id` is `-1`, it means this department is in the root - level. + `parent_id` is `-1`, it means this department is the top-level + department. security: - AccountTokenAuth: [] responses: @@ -6317,21 +6321,99 @@ paths: application/json: schema: type: object + properties: + departments: + type: array + items: + type: object example: departments: - - id: 10 - name: Hulk's department - owner: Hulk - created_at: "2021-08-25T08:35:52+00:00" - parent_group_id: -1 - quota: -2 - /api/v2.1/address-book/departments/{department_id}/members/: + - id: 1 + name: Sys Dev + parent_id: -1 + org_id: -1 + id_in_org: 1 + - id: 2 + name: Developers + parent_id: 1 + org_id: -1 + id_in_org: 2 + /api/v2.1/address-book-v2/user-departments/: get: tags: - Departments - summary: List Deparment Members - operationId: listDeparmentMembers - description: List the members of a department in your team (organization). + summary: List User's Departments + operationId: listUserDepartments + description: >- + List the departments you are a member of. Every department is returned + together with its tree of `sub_departments`. Departments that are + already contained in the tree of another returned department are not + listed again. + security: + - AccountTokenAuth: [] + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + department_list: + type: array + items: + type: object + example: + department_list: + - id: 1 + name: Sys Dev + parent_id: -1 + org_id: -1 + id_in_org: 1 + sub_departments: + - id: 2 + name: Developers + parent_id: 1 + org_id: -1 + id_in_org: 2 + sub_departments: [] + /api/v2.1/address-book-v2/departments/{department_id}/sub-departments/: + get: + tags: + - Departments + summary: List Sub-Departments + operationId: listSubDepartments + description: List the direct sub-departments of a department. + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/department_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + department_list: + type: array + items: + type: object + example: + department_list: + - id: 2 + name: Developers + parent_id: 1 + org_id: -1 + id_in_org: 2 + /api/v2.1/address-book-v2/departments/{department_id}/members/: + get: + tags: + - Departments + summary: List Department Members + operationId: listDepartmentMembers + description: List the members of a department in your system or team (organization). security: - AccountTokenAuth: [] parameters: @@ -6343,20 +6425,226 @@ paths: application/json: schema: type: object + properties: + member_list: + type: array + items: + type: object example: - members: + member_list: - email: df0b9a4ca7f24d899a3039d082123456@auth.local name: Thor contact_email: thor@seatable.io - avatar_url: "" + avatar_url: https://cloud.seatable.io/media/avatars/default.png + is_staff: false is_admin: false role: Member - email: 51a96159d5334afc9914561187654321@auth.local name: Hulk contact_email: hulk@seatable.io - avatar_url: "" + avatar_url: https://cloud.seatable.io/media/avatars/default.png + is_staff: true + is_admin: true + role: Admin + post: + tags: + - Departments + summary: Add Department Members + operationId: addDepartmentMembers + description: >- + Add one or more users to a department. You have to be an admin of the + department, and the department has to have a group. + requestBody: + content: + application/json: + schema: + type: object + required: + - emails + properties: + emails: + $ref: "#/components/schemas/department_member_emails" + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/department_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + success: + type: array + items: + type: object + failed: + type: array + items: + type: object + example: + success: + - group_id: 48 + name: Thor + email: df0b9a4ca7f24d899a3039d082123456@auth.local + contact_email: thor@seatable.io + login_id: "" + avatar_url: https://cloud.seatable.io/media/avatars/default.png is_admin: false role: Member + department_id: 2 + failed: + - email: 51a96159d5334afc9914561187654321@auth.local + error_msg: User Hulk is already a department member. + /api/v2.1/address-book-v2/departments/{department_id}/members/{user_id}/: + put: + tags: + - Departments + summary: Update Department Member + operationId: updateDepartmentMember + description: >- + Promote a department member to department admin or demote them. You + have to be an admin of the department, and the department has to have + a group. + requestBody: + content: + application/json: + schema: + type: object + properties: + is_admin: + $ref: "#/components/schemas/is_admin" + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/department_id" + - $ref: "#/components/parameters/user_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + group_id: + type: integer + name: + type: string + email: + type: string + contact_email: + type: string + login_id: + type: string + avatar_url: + type: string + is_admin: + type: boolean + role: + type: string + department_id: + type: integer + example: + group_id: 48 + name: Thor + email: df0b9a4ca7f24d899a3039d082123456@auth.local + contact_email: thor@seatable.io + login_id: "" + avatar_url: https://cloud.seatable.io/media/avatars/default.png + is_admin: true + role: Admin + department_id: 2 + delete: + tags: + - Departments + summary: Remove Department Member + operationId: removeDepartmentMember + description: >- + Remove a user from a department. You have to be an admin of the + department, and the department has to have a group. + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/department_id" + - $ref: "#/components/parameters/user_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + example: + success: true + /api/v2.1/address-book-v2/departments/{department_id}/members/{user_id}/dtables/: + get: + tags: + - Departments + summary: List Department Member's Bases + operationId: listDepartmentMemberBases + description: >- + List the personal bases of a department member. You have to be a + member of one of the department's ancestor departments. + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/department_id" + - $ref: "#/components/parameters/user_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + dtable_list: + type: array + items: + type: object + example: + dtable_list: + - id: 100 + workspace_id: 200 + uuid: 12345678-1f00-46af-98df-4007b3635e1c + name: CRM + created_at: "2021-03-02T10:15:40+00:00" + updated_at: "2021-03-02T10:18:01+00:00" + color: null + text_color: null + icon: null + is_encrypted: false + in_storage: true + backend: js + /api/v2.1/address-book-v2/departments/groups/{group_id}/members-count/: + get: + tags: + - Departments + summary: Get Department Group Members Count + operationId: getDepartmentGroupMembersCount + description: >- + Get the number of members of a department group. The members of a + department group are the members of the department and of all its + sub-departments. + security: + - AccountTokenAuth: [] + parameters: + - $ref: "#/components/parameters/group_id" + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + count: + type: integer + example: + count: 12 # Forms /api/v2.1/forms/: