pjdev-gitlab 5.3.0__tar.gz → 5.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/PKG-INFO +1 -1
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/__about__.py +1 -1
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/users_service.py +80 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/test_report_pjdev-gitlab.txt +119 -110
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_users_service.py +89 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/.gitignore +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/LICENSE.txt +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/README.md +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/pyproject.toml +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_issues/SKILL.md +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_merge_requests/SKILL.md +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_packages/SKILL.md +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_repo_files/SKILL.md +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_work_items/SKILL.md +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/__init__.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/api_utilities.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/config_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/git_sync_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/issues_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/merge_requests_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/models.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/notes_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/oauth_cli.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/oauth_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/packages_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/py.typed +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/repo_files_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/sync_cli.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/src/pjdev_gitlab/work_items_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/test.sh +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/__init__.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/conftest.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_api_utilities.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_git_sync_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_issues_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_merge_requests_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_models.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_notes_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_oauth_cli.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_oauth_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_packages_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_repo_files_service.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_sync_cli.py +0 -0
- {pjdev_gitlab-5.3.0 → pjdev_gitlab-5.4.0}/tests/tests_for_work_items_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pjdev-gitlab
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.4.0
|
|
4
4
|
Project-URL: Documentation, https://gitlab.purplejay.io/keystone/python/-/tree/main/pjdev-gitlab/README.md
|
|
5
5
|
Project-URL: Issues, https://gitlab.purplejay.io/keystone/python/-/issues
|
|
6
6
|
Project-URL: Source, https://gitlab.purplejay.io/keystone/python
|
|
@@ -146,6 +146,86 @@ async def get_user(user_id: Any, *, client: Optional[AsyncClient] = None) -> Use
|
|
|
146
146
|
return User.model_validate(row)
|
|
147
147
|
|
|
148
148
|
|
|
149
|
+
class UserUpdateNotApplied(RuntimeError):
|
|
150
|
+
"""GitLab answered 200 but returned a record that does not reflect the change.
|
|
151
|
+
|
|
152
|
+
``PUT /users/:id`` drops attributes it will not apply instead of rejecting
|
|
153
|
+
the request -- an administrator cannot be flagged ``external``, for one.
|
|
154
|
+
Without this check a caller marking seats external reads the 200 as done
|
|
155
|
+
while the seat stays billable, which is the same class of confident-wrong
|
|
156
|
+
answer the projection check above exists to prevent.
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@async_retry_http(status_codes_to_ignore=_IGNORE_4XX)
|
|
161
|
+
async def _put_user_row(
|
|
162
|
+
user_id: Any, payload: dict[str, Any], client: Optional[AsyncClient]
|
|
163
|
+
) -> dict[str, Any]:
|
|
164
|
+
async def _exec(_client: AsyncClient) -> dict[str, Any]:
|
|
165
|
+
r = await _client.put(f"/users/{encode_path_segment(user_id)}", json=payload)
|
|
166
|
+
r.raise_for_status()
|
|
167
|
+
return r.json()
|
|
168
|
+
|
|
169
|
+
if client is None:
|
|
170
|
+
async with http_client() as _client:
|
|
171
|
+
return await _exec(_client)
|
|
172
|
+
return await _exec(client)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
async def update_user(
|
|
176
|
+
user_id: Any,
|
|
177
|
+
*,
|
|
178
|
+
external: Optional[bool] = None,
|
|
179
|
+
extra_fields: Optional[dict[str, Any]] = None,
|
|
180
|
+
client: Optional[AsyncClient] = None,
|
|
181
|
+
) -> User:
|
|
182
|
+
"""Update a user via ``PUT /users/:id`` and return the updated record.
|
|
183
|
+
|
|
184
|
+
Admin-only, and the response is the admin projection, so a credential
|
|
185
|
+
without admin mode raises :class:`AdminModeRequired` (or is refused
|
|
186
|
+
outright with a 403). ``external`` is sent as a JSON boolean -- the string
|
|
187
|
+
``"true"`` dance in :func:`list_users` applies to query parameters, not to
|
|
188
|
+
a request body -- and passing ``None`` leaves the flag alone rather than
|
|
189
|
+
clearing it.
|
|
190
|
+
|
|
191
|
+
``extra_fields`` is merged into the payload for attributes without a named
|
|
192
|
+
parameter here. Only ``external`` is verified against the response:
|
|
193
|
+
GitLab defers some writes rather than applying them (an ``email`` change
|
|
194
|
+
waits on confirmation and echoes back the old address), so checking every
|
|
195
|
+
field would report those as failures.
|
|
196
|
+
"""
|
|
197
|
+
payload: dict[str, Any] = dict(extra_fields or {})
|
|
198
|
+
if external is not None:
|
|
199
|
+
payload["external"] = external
|
|
200
|
+
if not payload:
|
|
201
|
+
raise ValueError("update_user needs at least one field to change")
|
|
202
|
+
|
|
203
|
+
row = await _put_user_row(user_id, payload, client)
|
|
204
|
+
|
|
205
|
+
# Both checks sit outside the retry decorator on purpose: it funnels every
|
|
206
|
+
# exception into an ExceptionGroup, which would bury the message.
|
|
207
|
+
assert_admin_projection(row)
|
|
208
|
+
if external is not None and row.get("external") != external:
|
|
209
|
+
raise UserUpdateNotApplied(
|
|
210
|
+
f"GitLab accepted PUT /users/{user_id} but returned "
|
|
211
|
+
f"external={row.get('external')!r} after a request for "
|
|
212
|
+
f"external={external!r}. GitLab refuses to flag an administrator "
|
|
213
|
+
"external, and drops the attribute silently when it does."
|
|
214
|
+
)
|
|
215
|
+
return User.model_validate(row)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
async def set_user_external(
|
|
219
|
+
user_id: Any, external: bool = True, *, client: Optional[AsyncClient] = None
|
|
220
|
+
) -> User:
|
|
221
|
+
"""Flag a user ``external``, or clear the flag with ``external=False``.
|
|
222
|
+
|
|
223
|
+
External users do not consume a billable seat, so this is the write half of
|
|
224
|
+
:func:`list_external_users`.
|
|
225
|
+
"""
|
|
226
|
+
return await update_user(user_id, external=external, client=client)
|
|
227
|
+
|
|
228
|
+
|
|
149
229
|
@async_retry_http(status_codes_to_ignore=_IGNORE_4XX)
|
|
150
230
|
async def list_memberships(
|
|
151
231
|
user_id: Any, *, client: Optional[AsyncClient] = None
|
|
@@ -5,134 +5,143 @@ rootdir: /builds/keystone/python/pjdev-gitlab
|
|
|
5
5
|
configfile: pyproject.toml
|
|
6
6
|
plugins: anyio-4.14.2, respx-0.23.1, asyncio-1.4.0
|
|
7
7
|
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
|
|
8
|
-
collecting ... collected
|
|
8
|
+
collecting ... collected 136 items
|
|
9
9
|
|
|
10
10
|
tests/tests_for_api_utilities.py::test_build_auth_defaults_to_private_token PASSED [ 0%]
|
|
11
11
|
tests/tests_for_api_utilities.py::test_build_auth_selects_bearer_for_oauth PASSED [ 1%]
|
|
12
12
|
tests/tests_for_api_utilities.py::test_bearer_scheme_sends_authorization_header PASSED [ 2%]
|
|
13
|
-
tests/tests_for_api_utilities.py::test_encode_path_segment PASSED [
|
|
13
|
+
tests/tests_for_api_utilities.py::test_encode_path_segment PASSED [ 2%]
|
|
14
14
|
tests/tests_for_api_utilities.py::test_paginate_consumes_x_next_page PASSED [ 3%]
|
|
15
15
|
tests/tests_for_api_utilities.py::test_async_retry_http_aborts_on_ignored_status PASSED [ 4%]
|
|
16
16
|
tests/tests_for_api_utilities.py::test_async_retry_http_retries_on_5xx PASSED [ 5%]
|
|
17
|
-
tests/tests_for_api_utilities.py::test_upload_to_project_returns_markdown PASSED [
|
|
18
|
-
tests/tests_for_git_sync_service.py::test_build_remote_url[https://gitlab.example.com-g/s/n-https://gitlab.example.com/g/s/n.git] PASSED [
|
|
17
|
+
tests/tests_for_api_utilities.py::test_upload_to_project_returns_markdown PASSED [ 5%]
|
|
18
|
+
tests/tests_for_git_sync_service.py::test_build_remote_url[https://gitlab.example.com-g/s/n-https://gitlab.example.com/g/s/n.git] PASSED [ 6%]
|
|
19
19
|
tests/tests_for_git_sync_service.py::test_build_remote_url[https://gitlab.example.com/-/g/n/-https://gitlab.example.com/g/n.git] PASSED [ 7%]
|
|
20
20
|
tests/tests_for_git_sync_service.py::test_build_remote_url[https://gitlab.example.com-g/n.git-https://gitlab.example.com/g/n.git] PASSED [ 8%]
|
|
21
|
-
tests/tests_for_git_sync_service.py::test_build_remote_url_empty_repo_errors PASSED [
|
|
22
|
-
tests/tests_for_git_sync_service.py::test_default_target_dir[developers/claude-skills-claude-skills] PASSED [
|
|
23
|
-
tests/tests_for_git_sync_service.py::test_default_target_dir[g/s/name.git-name] PASSED [
|
|
21
|
+
tests/tests_for_git_sync_service.py::test_build_remote_url_empty_repo_errors PASSED [ 8%]
|
|
22
|
+
tests/tests_for_git_sync_service.py::test_default_target_dir[developers/claude-skills-claude-skills] PASSED [ 9%]
|
|
23
|
+
tests/tests_for_git_sync_service.py::test_default_target_dir[g/s/name.git-name] PASSED [ 10%]
|
|
24
24
|
tests/tests_for_git_sync_service.py::test_default_target_dir[solo-solo] PASSED [ 11%]
|
|
25
|
-
tests/tests_for_git_sync_service.py::test_clone_when_target_missing PASSED [
|
|
26
|
-
tests/tests_for_git_sync_service.py::test_clone_passes_token_via_askpass_env_only PASSED [
|
|
27
|
-
tests/tests_for_git_sync_service.py::test_clone_with_branch PASSED [
|
|
28
|
-
tests/tests_for_git_sync_service.py::test_update_when_target_is_repo PASSED [
|
|
29
|
-
tests/tests_for_git_sync_service.py::test_update_with_existing_branch_checks_out_and_ff PASSED [
|
|
30
|
-
tests/tests_for_git_sync_service.py::test_update_with_new_branch_creates_from_fetch_head PASSED [
|
|
31
|
-
tests/tests_for_git_sync_service.py::test_update_adds_origin_when_absent PASSED [
|
|
32
|
-
tests/tests_for_git_sync_service.py::test_update_detached_head_errors PASSED [
|
|
33
|
-
tests/tests_for_git_sync_service.py::test_clone_failure_removes_partial_target PASSED [
|
|
34
|
-
tests/tests_for_git_sync_service.py::test_target_exists_but_not_a_repo_errors PASSED [
|
|
35
|
-
tests/tests_for_git_sync_service.py::test_missing_token_errors PASSED [
|
|
36
|
-
tests/tests_for_git_sync_service.py::test_ff_only_failure_propagates PASSED [
|
|
37
|
-
tests/tests_for_issues_service.py::test_search_issues_paginates PASSED [
|
|
38
|
-
tests/tests_for_issues_service.py::test_create_issue_with_image_uploads_first_then_creates PASSED [
|
|
39
|
-
tests/tests_for_issues_service.py::test_set_issue_state PASSED [
|
|
40
|
-
tests/tests_for_issues_service.py::test_set_issue_labels_modes PASSED [
|
|
41
|
-
tests/tests_for_issues_service.py::test_aggregate_issues_by_label PASSED [
|
|
42
|
-
tests/tests_for_issues_service.py::test_comment_on_issue PASSED [
|
|
43
|
-
tests/tests_for_issues_service.py::test_list_issue_notes_delegates_to_notes_service PASSED [
|
|
44
|
-
tests/tests_for_merge_requests_service.py::test_get_merge_request PASSED [
|
|
45
|
-
tests/tests_for_merge_requests_service.py::test_comment_on_merge_request PASSED [
|
|
46
|
-
tests/tests_for_merge_requests_service.py::test_inline_diff_comment_payload_shape PASSED [
|
|
47
|
-
tests/tests_for_merge_requests_service.py::test_approve_and_unapprove PASSED [
|
|
48
|
-
tests/tests_for_merge_requests_service.py::test_list_merge_request_notes_delegates_to_notes_service PASSED [
|
|
49
|
-
tests/tests_for_models.py::test_config_api_base_url_strips_trailing_slash PASSED [
|
|
50
|
-
tests/tests_for_models.py::test_issue_extra_fields_ignored PASSED [
|
|
51
|
-
tests/tests_for_models.py::test_merge_request_basic PASSED [
|
|
52
|
-
tests/tests_for_models.py::test_config_env_loading PASSED [
|
|
53
|
-
tests/tests_for_notes_service.py::test_list_notes_for_merge_request PASSED [
|
|
54
|
-
tests/tests_for_notes_service.py::test_list_notes_for_issue_uses_the_issue_path PASSED [
|
|
55
|
-
tests/tests_for_notes_service.py::test_list_notes_can_drop_system_notes PASSED [
|
|
56
|
-
tests/tests_for_notes_service.py::test_list_notes_forwards_sort_and_order_by PASSED [
|
|
57
|
-
tests/tests_for_notes_service.py::test_get_note PASSED [
|
|
58
|
-
tests/tests_for_notes_service.py::test_list_discussions_groups_notes_into_threads PASSED [
|
|
59
|
-
tests/tests_for_notes_service.py::test_list_discussions_dropping_system_notes_drops_emptied_threads PASSED [
|
|
60
|
-
tests/tests_for_notes_service.py::test_get_discussion PASSED [
|
|
61
|
-
tests/tests_for_notes_service.py::test_reply_to_discussion_posts_to_the_thread PASSED [
|
|
62
|
-
tests/tests_for_notes_service.py::test_reply_to_note_resolves_the_containing_thread PASSED [
|
|
63
|
-
tests/tests_for_notes_service.py::test_reply_to_note_raises_when_the_note_is_not_there PASSED [
|
|
64
|
-
tests/tests_for_notes_service.py::test_find_discussion_for_note_returns_none_when_absent PASSED [
|
|
65
|
-
tests/tests_for_notes_service.py::test_quick_action_only_reply_raises_a_useful_error PASSED [
|
|
66
|
-
tests/tests_for_notes_service.py::test_notes_paginate_across_pages PASSED [
|
|
67
|
-
tests/tests_for_oauth_cli.py::test_missing_client_id_errors PASSED [
|
|
68
|
-
tests/tests_for_oauth_cli.py::test_success_prints_only_token PASSED [
|
|
69
|
-
tests/tests_for_oauth_cli.py::test_reads_config_from_env PASSED [
|
|
70
|
-
tests/tests_for_oauth_cli.py::test_bad_redirect_port_errors PASSED [
|
|
71
|
-
tests/tests_for_oauth_cli.py::test_oauth_error_returns_one PASSED [
|
|
72
|
-
tests/tests_for_oauth_service.py::test_make_pkce_challenge_is_s256_of_verifier PASSED [
|
|
73
|
-
tests/tests_for_oauth_service.py::test_cache_path_is_per_host_and_private PASSED [
|
|
74
|
-
tests/tests_for_oauth_service.py::test_token_if_valid_expiry PASSED [
|
|
75
|
-
tests/tests_for_oauth_service.py::test_store_token_response_roundtrip PASSED [
|
|
76
|
-
tests/tests_for_oauth_service.py::test_store_token_response_requires_access_token PASSED [
|
|
77
|
-
tests/tests_for_oauth_service.py::test_refresh_access_token_success PASSED [
|
|
78
|
-
tests/tests_for_oauth_service.py::test_refresh_access_token_returns_none_on_error PASSED [
|
|
79
|
-
tests/tests_for_oauth_service.py::test_get_access_token_reuses_valid_cache PASSED [
|
|
80
|
-
tests/tests_for_oauth_service.py::test_get_access_token_refreshes_expired_cache PASSED [
|
|
81
|
-
tests/tests_for_oauth_service.py::test_get_access_token_requires_client_id PASSED [
|
|
82
|
-
tests/tests_for_oauth_service.py::test_parse_scopes_accepts_space_and_comma_separated PASSED [
|
|
83
|
-
tests/tests_for_oauth_service.py::test_scopes_satisfy_is_subset_check PASSED [
|
|
84
|
-
tests/tests_for_oauth_service.py::test_with_admin_mode_is_idempotent_and_stable PASSED [
|
|
85
|
-
tests/tests_for_oauth_service.py::test_token_if_valid_rejects_narrower_cached_scope PASSED [
|
|
86
|
-
tests/tests_for_oauth_service.py::test_get_access_token_does_not_reuse_api_token_for_admin_mode PASSED [
|
|
87
|
-
tests/tests_for_oauth_service.py::test_get_access_token_reuses_cache_that_already_has_admin_mode PASSED [
|
|
88
|
-
tests/tests_for_oauth_service.py::test_get_access_token_rejects_a_grant_missing_the_requested_scope PASSED [
|
|
89
|
-
tests/tests_for_packages_service.py::test_upload_generic_package_url_shape PASSED [
|
|
90
|
-
tests/tests_for_packages_service.py::test_download_generic_package PASSED [
|
|
91
|
-
tests/tests_for_packages_service.py::test_list_packages PASSED [
|
|
92
|
-
tests/tests_for_repo_files_service.py::test_get_file_url_encoded PASSED [
|
|
93
|
-
tests/tests_for_repo_files_service.py::test_download_file_writes_to_dest PASSED [
|
|
94
|
-
tests/tests_for_repo_files_service.py::test_download_directory_walks_tree PASSED [
|
|
95
|
-
tests/tests_for_sync_cli.py::test_missing_repo_errors PASSED [
|
|
96
|
-
tests/tests_for_sync_cli.py::test_missing_client_id_errors PASSED [
|
|
97
|
-
tests/tests_for_sync_cli.py::test_success_prints_path_and_forwards_args PASSED [
|
|
98
|
-
tests/tests_for_sync_cli.py::test_reads_config_from_env PASSED [
|
|
99
|
-
tests/tests_for_sync_cli.py::test_oauth_error_returns_one PASSED [
|
|
100
|
-
tests/tests_for_sync_cli.py::test_sync_error_returns_one PASSED [
|
|
101
|
-
tests/tests_for_sync_cli.py::test_bad_redirect_port_errors PASSED [
|
|
102
|
-
tests/tests_for_users_service.py::test_assert_admin_projection_accepts_admin_row PASSED [
|
|
103
|
-
tests/tests_for_users_service.py::test_assert_admin_projection_names_the_missing_fields PASSED [
|
|
104
|
-
tests/tests_for_users_service.py::test_list_users_returns_admin_fields PASSED [
|
|
105
|
-
tests/tests_for_users_service.py::test_list_users_raises_without_admin_mode PASSED [
|
|
106
|
-
tests/tests_for_users_service.py::test_list_users_sends_only_true_filters PASSED [
|
|
107
|
-
tests/tests_for_users_service.py::test_list_external_users_filters_on_the_flag_not_the_query_param PASSED [
|
|
108
|
-
tests/tests_for_users_service.py::test_list_external_users_can_include_bots PASSED [
|
|
109
|
-
tests/tests_for_users_service.py::test_get_user_requires_admin_projection PASSED [
|
|
110
|
-
tests/
|
|
111
|
-
tests/
|
|
112
|
-
tests/
|
|
113
|
-
tests/
|
|
114
|
-
tests/
|
|
115
|
-
tests/
|
|
116
|
-
tests/
|
|
117
|
-
tests/
|
|
118
|
-
tests/
|
|
119
|
-
tests/tests_for_work_items_service.py::
|
|
120
|
-
tests/tests_for_work_items_service.py::
|
|
25
|
+
tests/tests_for_git_sync_service.py::test_clone_when_target_missing PASSED [ 11%]
|
|
26
|
+
tests/tests_for_git_sync_service.py::test_clone_passes_token_via_askpass_env_only PASSED [ 12%]
|
|
27
|
+
tests/tests_for_git_sync_service.py::test_clone_with_branch PASSED [ 13%]
|
|
28
|
+
tests/tests_for_git_sync_service.py::test_update_when_target_is_repo PASSED [ 13%]
|
|
29
|
+
tests/tests_for_git_sync_service.py::test_update_with_existing_branch_checks_out_and_ff PASSED [ 14%]
|
|
30
|
+
tests/tests_for_git_sync_service.py::test_update_with_new_branch_creates_from_fetch_head PASSED [ 15%]
|
|
31
|
+
tests/tests_for_git_sync_service.py::test_update_adds_origin_when_absent PASSED [ 16%]
|
|
32
|
+
tests/tests_for_git_sync_service.py::test_update_detached_head_errors PASSED [ 16%]
|
|
33
|
+
tests/tests_for_git_sync_service.py::test_clone_failure_removes_partial_target PASSED [ 17%]
|
|
34
|
+
tests/tests_for_git_sync_service.py::test_target_exists_but_not_a_repo_errors PASSED [ 18%]
|
|
35
|
+
tests/tests_for_git_sync_service.py::test_missing_token_errors PASSED [ 19%]
|
|
36
|
+
tests/tests_for_git_sync_service.py::test_ff_only_failure_propagates PASSED [ 19%]
|
|
37
|
+
tests/tests_for_issues_service.py::test_search_issues_paginates PASSED [ 20%]
|
|
38
|
+
tests/tests_for_issues_service.py::test_create_issue_with_image_uploads_first_then_creates PASSED [ 21%]
|
|
39
|
+
tests/tests_for_issues_service.py::test_set_issue_state PASSED [ 22%]
|
|
40
|
+
tests/tests_for_issues_service.py::test_set_issue_labels_modes PASSED [ 22%]
|
|
41
|
+
tests/tests_for_issues_service.py::test_aggregate_issues_by_label PASSED [ 23%]
|
|
42
|
+
tests/tests_for_issues_service.py::test_comment_on_issue PASSED [ 24%]
|
|
43
|
+
tests/tests_for_issues_service.py::test_list_issue_notes_delegates_to_notes_service PASSED [ 25%]
|
|
44
|
+
tests/tests_for_merge_requests_service.py::test_get_merge_request PASSED [ 25%]
|
|
45
|
+
tests/tests_for_merge_requests_service.py::test_comment_on_merge_request PASSED [ 26%]
|
|
46
|
+
tests/tests_for_merge_requests_service.py::test_inline_diff_comment_payload_shape PASSED [ 27%]
|
|
47
|
+
tests/tests_for_merge_requests_service.py::test_approve_and_unapprove PASSED [ 27%]
|
|
48
|
+
tests/tests_for_merge_requests_service.py::test_list_merge_request_notes_delegates_to_notes_service PASSED [ 28%]
|
|
49
|
+
tests/tests_for_models.py::test_config_api_base_url_strips_trailing_slash PASSED [ 29%]
|
|
50
|
+
tests/tests_for_models.py::test_issue_extra_fields_ignored PASSED [ 30%]
|
|
51
|
+
tests/tests_for_models.py::test_merge_request_basic PASSED [ 30%]
|
|
52
|
+
tests/tests_for_models.py::test_config_env_loading PASSED [ 31%]
|
|
53
|
+
tests/tests_for_notes_service.py::test_list_notes_for_merge_request PASSED [ 32%]
|
|
54
|
+
tests/tests_for_notes_service.py::test_list_notes_for_issue_uses_the_issue_path PASSED [ 33%]
|
|
55
|
+
tests/tests_for_notes_service.py::test_list_notes_can_drop_system_notes PASSED [ 33%]
|
|
56
|
+
tests/tests_for_notes_service.py::test_list_notes_forwards_sort_and_order_by PASSED [ 34%]
|
|
57
|
+
tests/tests_for_notes_service.py::test_get_note PASSED [ 35%]
|
|
58
|
+
tests/tests_for_notes_service.py::test_list_discussions_groups_notes_into_threads PASSED [ 36%]
|
|
59
|
+
tests/tests_for_notes_service.py::test_list_discussions_dropping_system_notes_drops_emptied_threads PASSED [ 36%]
|
|
60
|
+
tests/tests_for_notes_service.py::test_get_discussion PASSED [ 37%]
|
|
61
|
+
tests/tests_for_notes_service.py::test_reply_to_discussion_posts_to_the_thread PASSED [ 38%]
|
|
62
|
+
tests/tests_for_notes_service.py::test_reply_to_note_resolves_the_containing_thread PASSED [ 38%]
|
|
63
|
+
tests/tests_for_notes_service.py::test_reply_to_note_raises_when_the_note_is_not_there PASSED [ 39%]
|
|
64
|
+
tests/tests_for_notes_service.py::test_find_discussion_for_note_returns_none_when_absent PASSED [ 40%]
|
|
65
|
+
tests/tests_for_notes_service.py::test_quick_action_only_reply_raises_a_useful_error PASSED [ 41%]
|
|
66
|
+
tests/tests_for_notes_service.py::test_notes_paginate_across_pages PASSED [ 41%]
|
|
67
|
+
tests/tests_for_oauth_cli.py::test_missing_client_id_errors PASSED [ 42%]
|
|
68
|
+
tests/tests_for_oauth_cli.py::test_success_prints_only_token PASSED [ 43%]
|
|
69
|
+
tests/tests_for_oauth_cli.py::test_reads_config_from_env PASSED [ 44%]
|
|
70
|
+
tests/tests_for_oauth_cli.py::test_bad_redirect_port_errors PASSED [ 44%]
|
|
71
|
+
tests/tests_for_oauth_cli.py::test_oauth_error_returns_one PASSED [ 45%]
|
|
72
|
+
tests/tests_for_oauth_service.py::test_make_pkce_challenge_is_s256_of_verifier PASSED [ 46%]
|
|
73
|
+
tests/tests_for_oauth_service.py::test_cache_path_is_per_host_and_private PASSED [ 47%]
|
|
74
|
+
tests/tests_for_oauth_service.py::test_token_if_valid_expiry PASSED [ 47%]
|
|
75
|
+
tests/tests_for_oauth_service.py::test_store_token_response_roundtrip PASSED [ 48%]
|
|
76
|
+
tests/tests_for_oauth_service.py::test_store_token_response_requires_access_token PASSED [ 49%]
|
|
77
|
+
tests/tests_for_oauth_service.py::test_refresh_access_token_success PASSED [ 50%]
|
|
78
|
+
tests/tests_for_oauth_service.py::test_refresh_access_token_returns_none_on_error PASSED [ 50%]
|
|
79
|
+
tests/tests_for_oauth_service.py::test_get_access_token_reuses_valid_cache PASSED [ 51%]
|
|
80
|
+
tests/tests_for_oauth_service.py::test_get_access_token_refreshes_expired_cache PASSED [ 52%]
|
|
81
|
+
tests/tests_for_oauth_service.py::test_get_access_token_requires_client_id PASSED [ 52%]
|
|
82
|
+
tests/tests_for_oauth_service.py::test_parse_scopes_accepts_space_and_comma_separated PASSED [ 53%]
|
|
83
|
+
tests/tests_for_oauth_service.py::test_scopes_satisfy_is_subset_check PASSED [ 54%]
|
|
84
|
+
tests/tests_for_oauth_service.py::test_with_admin_mode_is_idempotent_and_stable PASSED [ 55%]
|
|
85
|
+
tests/tests_for_oauth_service.py::test_token_if_valid_rejects_narrower_cached_scope PASSED [ 55%]
|
|
86
|
+
tests/tests_for_oauth_service.py::test_get_access_token_does_not_reuse_api_token_for_admin_mode PASSED [ 56%]
|
|
87
|
+
tests/tests_for_oauth_service.py::test_get_access_token_reuses_cache_that_already_has_admin_mode PASSED [ 57%]
|
|
88
|
+
tests/tests_for_oauth_service.py::test_get_access_token_rejects_a_grant_missing_the_requested_scope PASSED [ 58%]
|
|
89
|
+
tests/tests_for_packages_service.py::test_upload_generic_package_url_shape PASSED [ 58%]
|
|
90
|
+
tests/tests_for_packages_service.py::test_download_generic_package PASSED [ 59%]
|
|
91
|
+
tests/tests_for_packages_service.py::test_list_packages PASSED [ 60%]
|
|
92
|
+
tests/tests_for_repo_files_service.py::test_get_file_url_encoded PASSED [ 61%]
|
|
93
|
+
tests/tests_for_repo_files_service.py::test_download_file_writes_to_dest PASSED [ 61%]
|
|
94
|
+
tests/tests_for_repo_files_service.py::test_download_directory_walks_tree PASSED [ 62%]
|
|
95
|
+
tests/tests_for_sync_cli.py::test_missing_repo_errors PASSED [ 63%]
|
|
96
|
+
tests/tests_for_sync_cli.py::test_missing_client_id_errors PASSED [ 63%]
|
|
97
|
+
tests/tests_for_sync_cli.py::test_success_prints_path_and_forwards_args PASSED [ 64%]
|
|
98
|
+
tests/tests_for_sync_cli.py::test_reads_config_from_env PASSED [ 65%]
|
|
99
|
+
tests/tests_for_sync_cli.py::test_oauth_error_returns_one PASSED [ 66%]
|
|
100
|
+
tests/tests_for_sync_cli.py::test_sync_error_returns_one PASSED [ 66%]
|
|
101
|
+
tests/tests_for_sync_cli.py::test_bad_redirect_port_errors PASSED [ 67%]
|
|
102
|
+
tests/tests_for_users_service.py::test_assert_admin_projection_accepts_admin_row PASSED [ 68%]
|
|
103
|
+
tests/tests_for_users_service.py::test_assert_admin_projection_names_the_missing_fields PASSED [ 69%]
|
|
104
|
+
tests/tests_for_users_service.py::test_list_users_returns_admin_fields PASSED [ 69%]
|
|
105
|
+
tests/tests_for_users_service.py::test_list_users_raises_without_admin_mode PASSED [ 70%]
|
|
106
|
+
tests/tests_for_users_service.py::test_list_users_sends_only_true_filters PASSED [ 71%]
|
|
107
|
+
tests/tests_for_users_service.py::test_list_external_users_filters_on_the_flag_not_the_query_param PASSED [ 72%]
|
|
108
|
+
tests/tests_for_users_service.py::test_list_external_users_can_include_bots PASSED [ 72%]
|
|
109
|
+
tests/tests_for_users_service.py::test_get_user_requires_admin_projection PASSED [ 73%]
|
|
110
|
+
tests/tests_for_users_service.py::test_update_user_sends_external_as_a_json_boolean PASSED [ 74%]
|
|
111
|
+
tests/tests_for_users_service.py::test_update_user_merges_extra_fields PASSED [ 75%]
|
|
112
|
+
tests/tests_for_users_service.py::test_update_user_refuses_an_empty_payload PASSED [ 75%]
|
|
113
|
+
tests/tests_for_users_service.py::test_update_user_leaves_external_alone_when_not_given PASSED [ 76%]
|
|
114
|
+
tests/tests_for_users_service.py::test_update_user_raises_when_the_change_was_dropped PASSED [ 77%]
|
|
115
|
+
tests/tests_for_users_service.py::test_update_user_requires_admin_projection_on_the_response PASSED [ 77%]
|
|
116
|
+
tests/tests_for_users_service.py::test_update_user_encodes_the_path_segment PASSED [ 78%]
|
|
117
|
+
tests/tests_for_users_service.py::test_set_user_external_defaults_to_marking_external PASSED [ 79%]
|
|
118
|
+
tests/tests_for_users_service.py::test_set_user_external_can_clear_the_flag PASSED [ 80%]
|
|
119
|
+
tests/tests_for_work_items_service.py::test_make_work_item_gid_passthrough_and_coerce PASSED [ 80%]
|
|
120
|
+
tests/tests_for_work_items_service.py::test_work_item_from_node_flattens_widgets PASSED [ 81%]
|
|
121
|
+
tests/tests_for_work_items_service.py::test_graphql_raises_on_errors PASSED [ 82%]
|
|
122
|
+
tests/tests_for_work_items_service.py::test_paginate_graphql_follows_end_cursor PASSED [ 83%]
|
|
123
|
+
tests/tests_for_work_items_service.py::test_get_work_item_by_global_id PASSED [ 83%]
|
|
124
|
+
tests/tests_for_work_items_service.py::test_get_work_item_by_project_path_and_iid_uses_namespace_lookup PASSED [ 84%]
|
|
125
|
+
tests/tests_for_work_items_service.py::test_get_work_item_omits_the_status_widget_by_default PASSED [ 85%]
|
|
126
|
+
tests/tests_for_work_items_service.py::test_get_work_item_include_status_selects_the_widget_and_lifts_it PASSED [ 86%]
|
|
127
|
+
tests/tests_for_work_items_service.py::test_get_work_item_missing_raises PASSED [ 86%]
|
|
128
|
+
tests/tests_for_work_items_service.py::test_get_work_item_status_by_project_path_and_iid PASSED [ 87%]
|
|
129
|
+
tests/tests_for_work_items_service.py::test_get_work_item_status_by_global_id_skips_the_iid_lookup PASSED [ 88%]
|
|
121
130
|
tests/tests_for_work_items_service.py::test_get_work_item_status_returns_none_when_no_status_is_assigned PASSED [ 88%]
|
|
122
|
-
tests/tests_for_work_items_service.py::test_get_work_item_status_missing_item_raises PASSED [
|
|
123
|
-
tests/tests_for_work_items_service.py::test_search_work_items_paginates_and_translates_state PASSED [
|
|
124
|
-
tests/tests_for_work_items_service.py::test_search_work_items_group_scope_uses_group_field PASSED [
|
|
131
|
+
tests/tests_for_work_items_service.py::test_get_work_item_status_missing_item_raises PASSED [ 89%]
|
|
132
|
+
tests/tests_for_work_items_service.py::test_search_work_items_paginates_and_translates_state PASSED [ 90%]
|
|
133
|
+
tests/tests_for_work_items_service.py::test_search_work_items_group_scope_uses_group_field PASSED [ 91%]
|
|
125
134
|
tests/tests_for_work_items_service.py::test_search_work_items_include_status_selects_the_widget PASSED [ 91%]
|
|
126
135
|
tests/tests_for_work_items_service.py::test_search_work_items_requires_one_scope PASSED [ 92%]
|
|
127
|
-
tests/tests_for_work_items_service.py::test_set_work_item_state_closes_via_workItemUpdate PASSED [
|
|
128
|
-
tests/tests_for_work_items_service.py::test_set_work_item_state_mutation_errors_raise PASSED [
|
|
136
|
+
tests/tests_for_work_items_service.py::test_set_work_item_state_closes_via_workItemUpdate PASSED [ 93%]
|
|
137
|
+
tests/tests_for_work_items_service.py::test_set_work_item_state_mutation_errors_raise PASSED [ 94%]
|
|
129
138
|
tests/tests_for_work_items_service.py::test_list_work_item_notes_filters_system_notes PASSED [ 94%]
|
|
130
139
|
tests/tests_for_work_items_service.py::test_comment_on_work_item_creates_note PASSED [ 95%]
|
|
131
140
|
tests/tests_for_work_items_service.py::test_reply_to_work_item_discussion_threads_via_discussion_id PASSED [ 96%]
|
|
132
|
-
tests/tests_for_work_items_service.py::test_set_work_item_labels_add_passes_addLabelIds PASSED [
|
|
141
|
+
tests/tests_for_work_items_service.py::test_set_work_item_labels_add_passes_addLabelIds PASSED [ 97%]
|
|
133
142
|
tests/tests_for_work_items_service.py::test_set_work_item_labels_remove_passes_removeLabelIds PASSED [ 97%]
|
|
134
143
|
tests/tests_for_work_items_service.py::test_list_labels_for_project_includes_ancestor_groups PASSED [ 98%]
|
|
135
144
|
tests/tests_for_work_items_service.py::test_create_label_passes_project_path_color_and_description PASSED [ 99%]
|
|
136
145
|
tests/tests_for_work_items_service.py::test_create_label_group_scope_passes_group_path PASSED [100%]
|
|
137
146
|
|
|
138
|
-
=============================
|
|
147
|
+
============================= 136 passed in 2.68s ==============================
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
1
3
|
import httpx
|
|
2
4
|
import pytest
|
|
3
5
|
import respx
|
|
@@ -99,3 +101,90 @@ async def test_get_user_requires_admin_projection():
|
|
|
99
101
|
)
|
|
100
102
|
with pytest.raises(AdminModeRequired):
|
|
101
103
|
await users_service.get_user(7)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@respx.mock
|
|
107
|
+
async def test_update_user_sends_external_as_a_json_boolean():
|
|
108
|
+
"""The string "true" is a query-parameter quirk; a body takes a real bool."""
|
|
109
|
+
route = respx.put("https://gitlab.example.com/api/v4/users/7").mock(
|
|
110
|
+
return_value=httpx.Response(200, json=ADMIN_ROW)
|
|
111
|
+
)
|
|
112
|
+
user = await users_service.update_user(7, external=True)
|
|
113
|
+
assert json.loads(route.calls.last.request.content) == {"external": True}
|
|
114
|
+
assert user.external is True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@respx.mock
|
|
118
|
+
async def test_update_user_merges_extra_fields():
|
|
119
|
+
route = respx.put("https://gitlab.example.com/api/v4/users/7").mock(
|
|
120
|
+
return_value=httpx.Response(200, json={**ADMIN_ROW, "name": "Renamed"})
|
|
121
|
+
)
|
|
122
|
+
await users_service.update_user(7, external=True, extra_fields={"name": "Renamed"})
|
|
123
|
+
assert json.loads(route.calls.last.request.content) == {
|
|
124
|
+
"name": "Renamed",
|
|
125
|
+
"external": True,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def test_update_user_refuses_an_empty_payload():
|
|
130
|
+
"""An empty PUT would report success while changing nothing."""
|
|
131
|
+
with pytest.raises(ValueError):
|
|
132
|
+
await users_service.update_user(7)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@respx.mock
|
|
136
|
+
async def test_update_user_leaves_external_alone_when_not_given():
|
|
137
|
+
route = respx.put("https://gitlab.example.com/api/v4/users/7").mock(
|
|
138
|
+
return_value=httpx.Response(200, json=ADMIN_ROW)
|
|
139
|
+
)
|
|
140
|
+
await users_service.update_user(7, extra_fields={"name": "Renamed"})
|
|
141
|
+
assert "external" not in json.loads(route.calls.last.request.content)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@respx.mock
|
|
145
|
+
async def test_update_user_raises_when_the_change_was_dropped():
|
|
146
|
+
"""200 with the flag unchanged -- what GitLab does for an administrator."""
|
|
147
|
+
respx.put("https://gitlab.example.com/api/v4/users/7").mock(
|
|
148
|
+
return_value=httpx.Response(200, json={**ADMIN_ROW, "external": False})
|
|
149
|
+
)
|
|
150
|
+
with pytest.raises(users_service.UserUpdateNotApplied) as excinfo:
|
|
151
|
+
await users_service.update_user(7, external=True)
|
|
152
|
+
assert "administrator" in str(excinfo.value)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@respx.mock
|
|
156
|
+
async def test_update_user_requires_admin_projection_on_the_response():
|
|
157
|
+
respx.put("https://gitlab.example.com/api/v4/users/7").mock(
|
|
158
|
+
return_value=httpx.Response(200, json=PUBLIC_ROW)
|
|
159
|
+
)
|
|
160
|
+
with pytest.raises(AdminModeRequired):
|
|
161
|
+
await users_service.update_user(7, external=True)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@respx.mock
|
|
165
|
+
async def test_update_user_encodes_the_path_segment():
|
|
166
|
+
route = respx.put("https://gitlab.example.com/api/v4/users/user%2Fname").mock(
|
|
167
|
+
return_value=httpx.Response(200, json=ADMIN_ROW)
|
|
168
|
+
)
|
|
169
|
+
await users_service.update_user("user/name", external=True)
|
|
170
|
+
assert route.called
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@respx.mock
|
|
174
|
+
async def test_set_user_external_defaults_to_marking_external():
|
|
175
|
+
route = respx.put("https://gitlab.example.com/api/v4/users/7").mock(
|
|
176
|
+
return_value=httpx.Response(200, json=ADMIN_ROW)
|
|
177
|
+
)
|
|
178
|
+
user = await users_service.set_user_external(7)
|
|
179
|
+
assert json.loads(route.calls.last.request.content) == {"external": True}
|
|
180
|
+
assert user.external is True
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@respx.mock
|
|
184
|
+
async def test_set_user_external_can_clear_the_flag():
|
|
185
|
+
route = respx.put("https://gitlab.example.com/api/v4/users/7").mock(
|
|
186
|
+
return_value=httpx.Response(200, json={**ADMIN_ROW, "external": False})
|
|
187
|
+
)
|
|
188
|
+
user = await users_service.set_user_external(7, False)
|
|
189
|
+
assert json.loads(route.calls.last.request.content) == {"external": False}
|
|
190
|
+
assert user.external is False
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|