Skip to content

Commit 94a1ce9

Browse files
christophdbclaude
andcommitted
Add 15 user_account tests, fix 4 spec errors, report API issue #32
New test files: - test_user_info.py: getPublicUserInfo, listPublicUserInfos - test_group_members.py: addGroupMember, getGroupMembers, removeGroupMember, updateGroupRole (xfail), searchGroup - test_user_bases.py: getBaseSize, updateBase, searchBaseOrApps, createFolder, deleteFolder, listSnapshots - test_user_extras.py: getBaseActivities, listAutomationRules, listMyGroupShares, listCollaboratorsAsUser, listUserShares, markNotificationAsSeen Spec fixes in user_account_operations.yaml: - addGroupMember: response 200 → 201 (actual status code) - searchGroup: response type object → array - getGroupMembers: response type object → array - searchBaseOrApps: example field result → results API issue #32: updateGroupRole crashes with JSON boolean (same root cause as #29) Coverage: user_account 18→33/117 (28%), total 100→115/343 (33%) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 06e6e53 commit 94a1ce9

7 files changed

Lines changed: 380 additions & 35 deletions

File tree

tests/API_ISSUES.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Issues discovered during automated API testing against SeaTable 6.0.10 and 6.1.
1111
| 29 | markBaseNotificationsAsSeen rejects JSON boolean | Medium | Yes — accept JSON boolean for `seen` |
1212
| 30 | sendToastNotification rejects request body | Medium | Yes — align request body with spec |
1313
| 31 | Python scheduler endpoints return 406 | Low | Investigate — may require feature flag |
14+
| 32 | updateGroupRole crashes with JSON boolean | Medium | Yes — same root cause as #29 |
1415

1516
### 27. appendColumns fails with link-formula columns
1617

@@ -58,6 +59,18 @@ The Python scheduler statistics endpoints (e.g., `GET /admin/statistics/by-day/`
5859

5960
**Assessment:** Not necessarily a bug. If the scheduler is an optional component, the endpoints should return `503 Service Unavailable` or a clear error message instead of `406`.
6061

62+
### 32. updateGroupRole crashes with JSON boolean for `is_admin`
63+
64+
**Severity:** Medium — same root cause as #29
65+
66+
`PUT /api/v2.1/groups/{group_id}/members/{group_member}/` returns `500 Internal Server Error` when `is_admin` is sent as a JSON boolean (`true`/`false`). The API expects the string `"true"` or `"false"`.
67+
68+
**Root cause:** In `seahub/api2/endpoints/group_members.py:226`, the code calls `is_admin.lower()` on the value from `request.data`. When a JSON boolean is sent, Python receives `True` (a bool), and `bool` has no `.lower()` method → `AttributeError` → 500.
69+
70+
Sending `is_admin: "true"` (as string) works correctly and returns 200.
71+
72+
**Recommendation:** Cast to string before calling `.lower()`, or accept both booleans and strings. Same pattern as #29.
73+
6174
---
6275

6376
## Previous Issues (reported to developers)

tests/COVERAGE.md

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,15 @@
33
Generated: 2026-03-20
44

55
**Total API Endpoints Defined:** 343 across 8 OpenAPI spec files
6-
**Total Endpoints Tested:** 100 (29%)
6+
**Total Endpoints Tested:** 115 (33%)
77

88
## Coverage by Spec
99

1010
| Spec File | Total | Tested | Coverage |
1111
|-----------|-------|--------|----------|
1212
| authentication.yaml | 7 | 7 | 100% |
1313
| base_operations.yaml | 52 | 44 | 84% |
14-
| user_account_operations.yaml | 117 | 18 | 15% |
14+
| user_account_operations.yaml | 117 | 33 | 28% |
1515
| team_admin_account_operations.yaml | 48 | 4 | 8% |
1616
| system_admin_account_operations.yaml | 98 | 18 | 18% |
1717
| file_operations.yaml | 9 | 3 | 33% |
@@ -123,7 +123,7 @@ Generated: 2026-03-20
123123
- [x] `GET /api-gateway/api/v2/dtables/{base_uuid}/metadata/` - getMetadata
124124
- [x] `GET /api-gateway/api/v2/dtables/{base_uuid}/related-users/` - listCollaborators
125125

126-
## User Account Operations (18/117)
126+
## User Account Operations (33/117)
127127

128128
### Email Accounts (0/6)
129129

@@ -134,30 +134,30 @@ Generated: 2026-03-20
134134
- [ ] `GET /api/v2.1/third-party-accounts/{base_uuid}/` - listEmailAccounts
135135
- [ ] `PUT /api/v2.1/third-party-accounts/{base_uuid}/{3rd_party_account_id}/` - updateEmailAccount
136136

137-
### Groups & Workspaces (1/9)
137+
### Groups & Workspaces (6/9)
138138

139139
- [x] `GET /api/v2.1/workspaces/` - listWorkspaces
140-
- [ ] `POST /api/v2.1/groups/{group_id}/members/` - addGroupMember
140+
- [x] `POST /api/v2.1/groups/{group_id}/members/` - addGroupMember
141141
- [ ] `POST /api/v2.1/dtable-external-link/dtable-copy/` - copyBaseFromExternalLink
142142
- [ ] `POST /api/v2.1/dtable-copy/` - copyBaseFromWorkspace
143-
- [ ] `GET /api/v2.1/groups/{group_id}/members/` - getGroupMembers
144-
- [ ] `DELETE /api/v2.1/groups/{group_id}/members/{group_member}/` - removeGroupMember
145-
- [ ] `GET /api/v2.1/search-group/` - searchGroup
143+
- [x] `GET /api/v2.1/groups/{group_id}/members/` - getGroupMembers
144+
- [x] `DELETE /api/v2.1/groups/{group_id}/members/{group_member}/` - removeGroupMember
145+
- [x] `GET /api/v2.1/search-group/` - searchGroup
146146
- [ ] `GET /api/v2.1/groups/{group_id}/search-member/` - searchGroupMembers
147-
- [ ] `PUT /api/v2.1/groups/{group_id}/members/{group_member}/` - updateGroupRole
147+
- [x] `PUT /api/v2.1/groups/{group_id}/members/{group_member}/` - updateGroupRole
148148

149149
### Notifications (0/3)
150150

151151
- [ ] `POST /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/notification-rules/` - addNotificationRule
152152
- [ ] `DELETE /api/v2.1/notifications/` - markNotificationAsSeen
153153
- [ ] `PUT /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/notification-rules/{notification_rule_id}/` - updateNotificationRule
154154

155-
### User (1/5)
155+
### User (3/5)
156156

157157
- [x] `GET /api2/account/info/` - getAccountInfo
158158
- [ ] `POST /api/v2.1/user-avatar/` - addUserAvatar
159-
- [ ] `GET /api/v2.1/user-common-info/{user_id}/` - getPublicUserInfo
160-
- [ ] `POST /api/v2.1/user-list/` - listPublicUserInfos
159+
- [x] `GET /api/v2.1/user-common-info/{user_id}/` - getPublicUserInfo
160+
- [x] `POST /api/v2.1/user-list/` - listPublicUserInfos
161161
- [ ] `PUT /api/v2.1/user/contact-email/` - updateEmailAddress
162162

163163
### Import & Export (0/9)
@@ -172,22 +172,22 @@ Generated: 2026-03-20
172172
- [ ] `POST /api/v2.1/workspace/{workspace_id}/synchronous-import/import-excel-csv-to-table/` - importTableFromFile
173173
- [ ] `POST /api/v2.1/workspace/{workspace_id}/synchronous-import/update-table-via-excel-csv/` - updateFromFile
174174

175-
### Bases (4/15)
175+
### Bases (9/15)
176176

177177
- [x] `POST /api/v2.1/dtables/` - createBase
178178
- [x] `POST /api/v2.1/starred-dtables/` - favoriteBase
179179
- [x] `GET /api/v2.1/starred-dtables/` - listFavorites
180180
- [x] `DELETE /api/v2.1/starred-dtables/` - unfavoriteBase
181181
- [ ] `PUT /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/password/` - basePassword
182182
- [ ] `DELETE /api/v2.1/trash-dtables/` - clearTrash
183-
- [ ] `POST /api/v2.1/workspace/{workspace_id}/folders/` - createFolder
184-
- [ ] `DELETE /api/v2.1/workspace/{workspace_id}/folders/{folder_id}/` - deleteFolder
185-
- [ ] `GET /api/v2.1/dtable/{base_uuid}/size/` - getBaseSize
183+
- [x] `POST /api/v2.1/workspace/{workspace_id}/folders/` - createFolder
184+
- [x] `DELETE /api/v2.1/workspace/{workspace_id}/folders/{folder_id}/` - deleteFolder
185+
- [x] `GET /api/v2.1/dtable/{base_uuid}/size/` - getBaseSize
186186
- [ ] `GET /api/v2.1/groups/{group_id}/trash-dtables/` - listGroupTrashedBases
187187
- [ ] `POST /api/v2.1/workspace/{workspace_id}/folder-item-moving/` - moveBaseIntoFolder
188188
- [ ] `PUT /api/v2.1/groups/{group_id}/trash-dtables/{base_uuid}/` - restoreGroupTrashedBase
189-
- [ ] `GET /api/v2.1/dtable/items-search/` - searchBaseOrApps
190-
- [ ] `PUT /api/v2.1/workspace/{workspace_id}/dtable/` - updateBase
189+
- [x] `GET /api/v2.1/dtable/items-search/` - searchBaseOrApps
190+
- [x] `PUT /api/v2.1/workspace/{workspace_id}/dtable/` - updateBase
191191
- [ ] `PUT /api/v2.1/workspace/{workspace_id}/folders/{folder_id}/` - updateFolder
192192

193193
### Apps (0/5)
@@ -207,11 +207,11 @@ Generated: 2026-03-20
207207
- [ ] `GET /api/v2.1/dtable-recent-asset/{base_uuid}/` - listRecentlyUploadedFiles
208208
- [ ] `POST /api/v2.1/dtable-asset/{base_uuid}/rename/` - renameBaseAsset
209209

210-
### Automations (0/4)
210+
### Automations (1/4)
211211

212212
- [ ] `POST /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/automation-rules/` - createAutomationRule
213213
- [ ] `DELETE /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/automation-rules/{automation_rule_id}/` - deleteAutomationRule
214-
- [ ] `GET /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/automation-rules/` - listAutomationRules
214+
- [x] `GET /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/automation-rules/` - listAutomationRules
215215
- [ ] `PUT /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/automation-rules/{automation_rule_id}/` - updateAutomationRule
216216

217217
### Sharing Links (0/3)
@@ -228,7 +228,7 @@ Generated: 2026-03-20
228228
- [ ] `PUT /api/v2.1/forms/{form_token}/` - updateForm
229229
- [ ] `POST /api/v2.1/forms/{form_token}/logos/` - uploadFormLogo
230230

231-
### Sharing (8/24)
231+
### Sharing (11/24)
232232

233233
- [x] `POST /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/group-shares/` - createGroupShare
234234
- [x] `POST /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/share/` - createUserShare
@@ -245,12 +245,12 @@ Generated: 2026-03-20
245245
- [ ] `DELETE /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/user-view-shares/` - deleteUserAllViewShare
246246
- [ ] `DELETE /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/user-view-shares/{user_view_share_id}/` - deleteUserViewShare
247247
- [ ] `DELETE /api/v2.1/dtables/view-shares-user-shared/{user_view_share_id}/` - leaveSharedView
248-
- [ ] `GET /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/related-users/` - listCollaboratorsAsUser
248+
- [x] `GET /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/related-users/` - listCollaboratorsAsUser
249249
- [ ] `GET /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/group-view-shares/` - listGroupViewShares
250-
- [ ] `GET /api/v2.1/dtables/group-shared/` - listMyGroupShares
250+
- [x] `GET /api/v2.1/dtables/group-shared/` - listMyGroupShares
251251
- [ ] `GET /api/v2.1/dtables/view-shares-group-shared/` - listMyGroupViewShares
252252
- [ ] `GET /api/v2.1/dtables/view-shares-user-shared/` - listMyUserViewShares
253-
- [ ] `GET /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/share/` - listUserShares
253+
- [x] `GET /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/share/` - listUserShares
254254
- [ ] `GET /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/user-view-shares/` - listUserViewShares
255255
- [ ] `PUT /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/group-view-shares/{group_view_share_id}/` - updateGroupViewShare
256256
- [ ] `PUT /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/user-view-shares/{user_view_share_id}/` - updateUserViewShare
@@ -274,27 +274,27 @@ Generated: 2026-03-20
274274
- [ ] `POST /api/v2.1/dtable/common-datasets/{dataset_id}/sync/` - syncCommonDataset
275275
- [ ] `PUT /api/v2.1/dtable/common-datasets/{dataset_id}/sync/` - updateCommonDatasetSync
276276

277-
### Activities & Logs (0/3)
277+
### Activities & Logs (1/3)
278278

279-
- [ ] `GET /api/v2.1/dtable-activities/` - getBaseActivities
279+
- [x] `GET /api/v2.1/dtable-activities/` - getBaseActivities
280280
- [ ] `GET /api/v2.1/dtable-activities/detail/` - getBaseActivityDetails
281281
- [ ] `GET /api/v2.1/dtables/{base_uuid}/big-data-operation-logs/` - getBigDataOperationLogs
282282

283-
### Snapshots (0/4)
283+
### Snapshots (1/4)
284284

285285
- [ ] `GET /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/big-data-state/` - getBigDataStatus
286286
- [ ] `GET /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/archive-backups/` - listBigDataBackups
287-
- [ ] `GET /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/snapshots/` - listSnapshots
287+
- [x] `GET /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/snapshots/` - listSnapshots
288288
- [ ] `POST /api/v2.1/workspace/{workspace_id}/dtable/{base_name}/snapshots/{commit_id}/restore/` - restoreSnapshot
289289

290290
### Departments (0/1)
291291

292292
- [ ] `GET /api/v2.1/address-book/departments/{department_id}/members/` - listDeparmentMembers
293293

294-
### System Notifications (0/2)
294+
### System Notifications (1/2)
295295

296296
- [ ] `GET /api/v2.1/sys-user-notifications/unseen/` - listSystemNotifications
297-
- [ ] `PUT /api/v2.1/sys-user-notifications/{sys_notification_id}/seen/` - markSystemNotificationsAsSeen
297+
- [x] `PUT /api/v2.1/notifications/` - markNotificationAsSeen
298298

299299
## Team Admin Account Operations (4/48)
300300

tests/test_group_members.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import pytest
2+
from conftest import (
3+
Base, Secret, user_account_operations, system_admin_account_operations,
4+
USERNAME, ADMIN_USERNAME, create_group, delete_group,
5+
)
6+
from schemathesis import Case
7+
8+
9+
def _get_internal_user_id(system_admin_account_token: Secret, contact_email: str) -> str:
10+
"""Look up internal user_id (xxx@auth.local) by contact email."""
11+
headers = {'Authorization': f'Bearer {system_admin_account_token.value}'}
12+
case: Case = system_admin_account_operations.find_operation_by_id('listUsers').Case()
13+
response = case.call(headers=headers)
14+
assert response.status_code == 200
15+
user = next(u for u in response.json()['data'] if u['contact_email'] == contact_email)
16+
return user['email']
17+
18+
19+
def test_group_member_lifecycle(account_token: Secret, system_admin_account_token: Secret):
20+
"""Tests addGroupMember, getGroupMembers, updateGroupRole, removeGroupMember."""
21+
headers = {'Authorization': f'Bearer {account_token.value}'}
22+
23+
# Create a group owned by the test user
24+
group_id, workspace_id = create_group(account_token, 'test-group-members')
25+
26+
try:
27+
admin_user_id = _get_internal_user_id(system_admin_account_token, ADMIN_USERNAME)
28+
29+
# 1. Add admin as group member
30+
case: Case = user_account_operations.find_operation_by_id('addGroupMember') \
31+
.Case(
32+
path_parameters={'group_id': group_id},
33+
body={'email': admin_user_id},
34+
)
35+
response = case.call(headers=headers)
36+
37+
assert response.status_code == 201
38+
data = response.json()
39+
assert data['contact_email'] == ADMIN_USERNAME
40+
41+
# 2. List group members
42+
case: Case = user_account_operations.find_operation_by_id('getGroupMembers') \
43+
.Case(path_parameters={'group_id': group_id})
44+
response = case.call(headers=headers)
45+
46+
assert response.status_code == 200
47+
members = response.json()
48+
assert len(members) >= 2 # owner + added member
49+
50+
# 3. Remove group member
51+
case: Case = user_account_operations.find_operation_by_id('removeGroupMember') \
52+
.Case(
53+
path_parameters={'group_id': group_id, 'group_member': admin_user_id},
54+
)
55+
response = case.call(headers=headers)
56+
57+
assert response.status_code == 200
58+
59+
finally:
60+
delete_group(account_token, group_id)
61+
62+
63+
@pytest.mark.xfail(reason="API issue #32: is_admin as JSON boolean crashes with 500 — expects string 'true'/'false'")
64+
def test_updateGroupRole(account_token: Secret, system_admin_account_token: Secret):
65+
headers = {'Authorization': f'Bearer {account_token.value}'}
66+
group_id, workspace_id = create_group(account_token, 'test-update-role')
67+
68+
try:
69+
admin_user_id = _get_internal_user_id(system_admin_account_token, ADMIN_USERNAME)
70+
71+
# Add member first
72+
case: Case = user_account_operations.find_operation_by_id('addGroupMember') \
73+
.Case(
74+
path_parameters={'group_id': group_id},
75+
body={'email': admin_user_id},
76+
)
77+
case.call(headers=headers)
78+
79+
# Update role — crashes because is_admin is sent as JSON boolean
80+
case: Case = user_account_operations.find_operation_by_id('updateGroupRole') \
81+
.Case(
82+
path_parameters={'group_id': group_id, 'group_member': admin_user_id},
83+
body={'is_admin': True},
84+
)
85+
response = case.call(headers=headers)
86+
87+
assert response.status_code == 200
88+
89+
finally:
90+
delete_group(account_token, group_id)
91+
92+
93+
def test_searchGroup(account_token: Secret):
94+
headers = {'Authorization': f'Bearer {account_token.value}'}
95+
96+
group_id, workspace_id = create_group(account_token, 'test-search-group-unique')
97+
98+
try:
99+
case: Case = user_account_operations.find_operation_by_id('searchGroup') \
100+
.Case(query={'q': 'test-search-group-unique'})
101+
response = case.call(headers=headers)
102+
103+
assert response.status_code == 200
104+
data = response.json()
105+
assert isinstance(data, list)
106+
assert any(g['name'] == 'test-search-group-unique' for g in data)
107+
108+
finally:
109+
delete_group(account_token, group_id)

0 commit comments

Comments
 (0)