pjdev-gitlab 5.1.9__tar.gz → 5.1.10__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.
Files changed (36) hide show
  1. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/PKG-INFO +18 -1
  2. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/README.md +17 -0
  3. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/pyproject.toml +3 -0
  4. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/__about__.py +1 -1
  5. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/__init__.py +1 -0
  6. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/api_utilities.py +1 -1
  7. pjdev_gitlab-5.1.10/src/pjdev_gitlab/oauth_cli.py +122 -0
  8. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/test_report_pjdev-gitlab.txt +58 -53
  9. pjdev_gitlab-5.1.10/tests/tests_for_oauth_cli.py +85 -0
  10. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/.gitignore +0 -0
  11. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/LICENSE.txt +0 -0
  12. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_issues/SKILL.md +0 -0
  13. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_merge_requests/SKILL.md +0 -0
  14. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_packages/SKILL.md +0 -0
  15. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_repo_files/SKILL.md +0 -0
  16. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_work_items/SKILL.md +0 -0
  17. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/config_service.py +0 -0
  18. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/issues_service.py +0 -0
  19. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/merge_requests_service.py +0 -0
  20. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/models.py +0 -0
  21. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/oauth_service.py +0 -0
  22. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/packages_service.py +0 -0
  23. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/py.typed +0 -0
  24. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/repo_files_service.py +0 -0
  25. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/work_items_service.py +0 -0
  26. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/test.sh +0 -0
  27. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/__init__.py +0 -0
  28. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/conftest.py +0 -0
  29. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_api_utilities.py +0 -0
  30. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_issues_service.py +0 -0
  31. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_merge_requests_service.py +0 -0
  32. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_models.py +0 -0
  33. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_oauth_service.py +0 -0
  34. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_packages_service.py +0 -0
  35. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_repo_files_service.py +0 -0
  36. {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_work_items_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pjdev-gitlab
3
- Version: 5.1.9
3
+ Version: 5.1.10
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
@@ -80,6 +80,23 @@ The OAuth application must be registered as a non-confidential (PKCE) app with
80
80
  is sent as `Authorization: Bearer <token>`; the default `private-token` scheme
81
81
  sends `PRIVATE-TOKEN` for personal/project/group access tokens.
82
82
 
83
+ #### `pjdev-gitlab-auth` console script
84
+
85
+ The same flow is exposed as a console script, so shell callers (e.g. skills) can
86
+ mint a token without embedding any Python. It prints only the token to stdout and
87
+ status to stderr:
88
+
89
+ ```bash
90
+ TOKEN="$(uvx --from 'pjdev-gitlab' pjdev-gitlab-auth \
91
+ --gitlab-url https://gitlab.example.com --client-id <client-id>)"
92
+ ```
93
+
94
+ The library is instance-agnostic: `--client-id` (or `GL_OAUTH_CLIENT_ID`) is
95
+ required and the host defaults to `https://gitlab.com` (override with
96
+ `--gitlab-url` / `GL_GITLAB_URL`). Other flags: `--client-secret`,
97
+ `--redirect-port`, `--scopes`, `--force` (each with a `GL_OAUTH_*` env
98
+ equivalent).
99
+
83
100
  ### Recommended: 1Password + `op run`
84
101
 
85
102
  On a developer laptop, keep the token in 1Password and inject it into the host
@@ -52,6 +52,23 @@ The OAuth application must be registered as a non-confidential (PKCE) app with
52
52
  is sent as `Authorization: Bearer <token>`; the default `private-token` scheme
53
53
  sends `PRIVATE-TOKEN` for personal/project/group access tokens.
54
54
 
55
+ #### `pjdev-gitlab-auth` console script
56
+
57
+ The same flow is exposed as a console script, so shell callers (e.g. skills) can
58
+ mint a token without embedding any Python. It prints only the token to stdout and
59
+ status to stderr:
60
+
61
+ ```bash
62
+ TOKEN="$(uvx --from 'pjdev-gitlab' pjdev-gitlab-auth \
63
+ --gitlab-url https://gitlab.example.com --client-id <client-id>)"
64
+ ```
65
+
66
+ The library is instance-agnostic: `--client-id` (or `GL_OAUTH_CLIENT_ID`) is
67
+ required and the host defaults to `https://gitlab.com` (override with
68
+ `--gitlab-url` / `GL_GITLAB_URL`). Other flags: `--client-secret`,
69
+ `--redirect-port`, `--scopes`, `--force` (each with a `GL_OAUTH_*` env
70
+ equivalent).
71
+
55
72
  ### Recommended: 1Password + `op run`
56
73
 
57
74
  On a developer laptop, keep the token in 1Password and inject it into the host
@@ -27,6 +27,9 @@ dependencies = [
27
27
  "pydantic>=2.12.5",
28
28
  ]
29
29
 
30
+ [project.scripts]
31
+ pjdev-gitlab-auth = "pjdev_gitlab.oauth_cli:main"
32
+
30
33
  [project.optional-dependencies]
31
34
  dev = [
32
35
  "ruff",
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2026-present Chris O'Neill <chris@purplejay.io>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "5.1.9"
4
+ __version__ = "5.1.10"
@@ -12,6 +12,7 @@ __all__ = [
12
12
  "issues_service",
13
13
  "merge_requests_service",
14
14
  "models",
15
+ "oauth_cli",
15
16
  "oauth_service",
16
17
  "packages_service",
17
18
  "repo_files_service",
@@ -2,7 +2,7 @@ import asyncio
2
2
  import ssl
3
3
  from contextlib import asynccontextmanager
4
4
  from functools import wraps
5
- from typing import Any, AsyncIterator, Callable, Dict, List, Optional
5
+ from typing import Any, AsyncIterator, Dict, List, Optional
6
6
  from urllib.parse import quote
7
7
 
8
8
  import httpx
@@ -0,0 +1,122 @@
1
+ """Command-line entry point: authenticate to GitLab and print an OAuth token.
2
+
3
+ Installed as the ``pjdev-gitlab-auth`` console script (see ``[project.scripts]``
4
+ in ``pyproject.toml``). Thin CLI over
5
+ :func:`pjdev_gitlab.oauth_service.get_access_token` that runs the OAuth 2.0
6
+ Authorization Code + PKCE browser flow so API writes are attributed to the human
7
+ invoking it rather than a shared bot. Prints ONLY the access token to stdout;
8
+ status goes to stderr. Tokens are cached + silently refreshed per host under
9
+ ``~/.config/pjdev-gitlab/tokens/``, so most runs need no interaction.
10
+
11
+ The library is instance-agnostic: the OAuth application's client id must be
12
+ supplied per call (there is no built-in default), and the GitLab host defaults to
13
+ ``https://gitlab.com``. Callers targeting another instance pass ``--gitlab-url``
14
+ (or ``GL_GITLAB_URL``) and ``--client-id`` (or ``GL_OAUTH_CLIENT_ID``).
15
+
16
+ Usage::
17
+
18
+ pjdev-gitlab-auth --gitlab-url https://gitlab.example.com --client-id <id>
19
+ # or, equivalently, via the environment:
20
+ GL_GITLAB_URL=https://gitlab.example.com GL_OAUTH_CLIENT_ID=<id> pjdev-gitlab-auth
21
+
22
+ The registered OAuth application must be a non-confidential (PKCE) app with the
23
+ requested scope (default ``api``) and Redirect URI exactly
24
+ ``http://localhost:<redirect-port>/callback``.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import argparse
30
+ import os
31
+ import sys
32
+
33
+ from pjdev_gitlab.oauth_service import (
34
+ DEFAULT_REDIRECT_PORT,
35
+ DEFAULT_SCOPES,
36
+ OAuthError,
37
+ get_access_token,
38
+ )
39
+
40
+ DEFAULT_GITLAB_URL = "https://gitlab.com"
41
+
42
+
43
+ def main() -> int:
44
+ parser = argparse.ArgumentParser(
45
+ prog="pjdev-gitlab-auth",
46
+ description="Authenticate to GitLab via OAuth and print a short-lived access token.",
47
+ )
48
+ parser.add_argument(
49
+ "--gitlab-url",
50
+ default=None,
51
+ help="GitLab base URL (or GL_GITLAB_URL; default https://gitlab.com)",
52
+ )
53
+ parser.add_argument(
54
+ "--client-id",
55
+ default=None,
56
+ help="registered OAuth app client id (or GL_OAUTH_CLIENT_ID) -- required",
57
+ )
58
+ parser.add_argument(
59
+ "--client-secret",
60
+ default=None,
61
+ help="only for a confidential app (or GL_OAUTH_CLIENT_SECRET)",
62
+ )
63
+ parser.add_argument(
64
+ "--redirect-port",
65
+ type=int,
66
+ default=None,
67
+ help=f"loopback redirect port (or GL_OAUTH_REDIRECT_PORT; default {DEFAULT_REDIRECT_PORT})",
68
+ )
69
+ parser.add_argument(
70
+ "--scopes",
71
+ default=None,
72
+ help=f'space-separated scopes (or GL_OAUTH_SCOPES; default "{DEFAULT_SCOPES}")',
73
+ )
74
+ parser.add_argument(
75
+ "--force",
76
+ action="store_true",
77
+ help="ignore any cached token and run the browser flow",
78
+ )
79
+ args = parser.parse_args()
80
+
81
+ gitlab_url = args.gitlab_url or os.environ.get("GL_GITLAB_URL") or DEFAULT_GITLAB_URL
82
+ client_id = args.client_id or os.environ.get("GL_OAUTH_CLIENT_ID")
83
+ if not client_id:
84
+ print(
85
+ "no OAuth client id: pass --client-id or set GL_OAUTH_CLIENT_ID to the "
86
+ "registered application's client id.",
87
+ file=sys.stderr,
88
+ )
89
+ return 2
90
+ client_secret = args.client_secret or os.environ.get("GL_OAUTH_CLIENT_SECRET") or None
91
+ scopes = args.scopes or os.environ.get("GL_OAUTH_SCOPES") or DEFAULT_SCOPES
92
+
93
+ port_raw = (
94
+ args.redirect_port
95
+ if args.redirect_port is not None
96
+ else os.environ.get("GL_OAUTH_REDIRECT_PORT", DEFAULT_REDIRECT_PORT)
97
+ )
98
+ try:
99
+ redirect_port = int(port_raw)
100
+ except (TypeError, ValueError):
101
+ print("redirect port must be an integer", file=sys.stderr)
102
+ return 2
103
+
104
+ try:
105
+ token = get_access_token(
106
+ gitlab_url=gitlab_url,
107
+ client_id=client_id,
108
+ client_secret=client_secret,
109
+ redirect_port=redirect_port,
110
+ scopes=scopes,
111
+ force=args.force,
112
+ )
113
+ except OAuthError as exc:
114
+ print(str(exc), file=sys.stderr)
115
+ return 1
116
+
117
+ print(token)
118
+ return 0
119
+
120
+
121
+ if __name__ == "__main__":
122
+ raise SystemExit(main())
@@ -5,65 +5,70 @@ rootdir: /builds/keystone/python/pjdev-gitlab
5
5
  configfile: pyproject.toml
6
6
  plugins: asyncio-1.4.0, respx-0.23.1, anyio-4.14.1
7
7
  asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
8
- collecting ... collected 58 items
8
+ collecting ... collected 63 items
9
9
 
10
10
  tests/tests_for_api_utilities.py::test_build_auth_defaults_to_private_token PASSED [ 1%]
11
11
  tests/tests_for_api_utilities.py::test_build_auth_selects_bearer_for_oauth PASSED [ 3%]
12
- tests/tests_for_api_utilities.py::test_bearer_scheme_sends_authorization_header PASSED [ 5%]
12
+ tests/tests_for_api_utilities.py::test_bearer_scheme_sends_authorization_header PASSED [ 4%]
13
13
  tests/tests_for_api_utilities.py::test_encode_path_segment PASSED [ 6%]
14
- tests/tests_for_api_utilities.py::test_paginate_consumes_x_next_page PASSED [ 8%]
15
- tests/tests_for_api_utilities.py::test_async_retry_http_aborts_on_ignored_status PASSED [ 10%]
16
- tests/tests_for_api_utilities.py::test_async_retry_http_retries_on_5xx PASSED [ 12%]
17
- tests/tests_for_api_utilities.py::test_upload_to_project_returns_markdown PASSED [ 13%]
18
- tests/tests_for_issues_service.py::test_search_issues_paginates PASSED [ 15%]
19
- tests/tests_for_issues_service.py::test_create_issue_with_image_uploads_first_then_creates PASSED [ 17%]
20
- tests/tests_for_issues_service.py::test_set_issue_state PASSED [ 18%]
21
- tests/tests_for_issues_service.py::test_set_issue_labels_modes PASSED [ 20%]
22
- tests/tests_for_issues_service.py::test_aggregate_issues_by_label PASSED [ 22%]
23
- tests/tests_for_issues_service.py::test_comment_on_issue PASSED [ 24%]
24
- tests/tests_for_merge_requests_service.py::test_get_merge_request PASSED [ 25%]
25
- tests/tests_for_merge_requests_service.py::test_comment_on_merge_request PASSED [ 27%]
26
- tests/tests_for_merge_requests_service.py::test_inline_diff_comment_payload_shape PASSED [ 29%]
27
- tests/tests_for_merge_requests_service.py::test_approve_and_unapprove PASSED [ 31%]
28
- tests/tests_for_models.py::test_config_api_base_url_strips_trailing_slash PASSED [ 32%]
29
- tests/tests_for_models.py::test_issue_extra_fields_ignored PASSED [ 34%]
30
- tests/tests_for_models.py::test_merge_request_basic PASSED [ 36%]
31
- tests/tests_for_models.py::test_config_env_loading PASSED [ 37%]
32
- tests/tests_for_oauth_service.py::test_make_pkce_challenge_is_s256_of_verifier PASSED [ 39%]
33
- tests/tests_for_oauth_service.py::test_cache_path_is_per_host_and_private PASSED [ 41%]
34
- tests/tests_for_oauth_service.py::test_token_if_valid_expiry PASSED [ 43%]
35
- tests/tests_for_oauth_service.py::test_store_token_response_roundtrip PASSED [ 44%]
36
- tests/tests_for_oauth_service.py::test_store_token_response_requires_access_token PASSED [ 46%]
37
- tests/tests_for_oauth_service.py::test_refresh_access_token_success PASSED [ 48%]
38
- tests/tests_for_oauth_service.py::test_refresh_access_token_returns_none_on_error PASSED [ 50%]
39
- tests/tests_for_oauth_service.py::test_get_access_token_reuses_valid_cache PASSED [ 51%]
40
- tests/tests_for_oauth_service.py::test_get_access_token_refreshes_expired_cache PASSED [ 53%]
41
- tests/tests_for_oauth_service.py::test_get_access_token_requires_client_id PASSED [ 55%]
42
- tests/tests_for_packages_service.py::test_upload_generic_package_url_shape PASSED [ 56%]
43
- tests/tests_for_packages_service.py::test_download_generic_package PASSED [ 58%]
44
- tests/tests_for_packages_service.py::test_list_packages PASSED [ 60%]
45
- tests/tests_for_repo_files_service.py::test_get_file_url_encoded PASSED [ 62%]
46
- tests/tests_for_repo_files_service.py::test_download_file_writes_to_dest PASSED [ 63%]
47
- tests/tests_for_repo_files_service.py::test_download_directory_walks_tree PASSED [ 65%]
48
- tests/tests_for_work_items_service.py::test_make_work_item_gid_passthrough_and_coerce PASSED [ 67%]
49
- tests/tests_for_work_items_service.py::test_work_item_from_node_flattens_widgets PASSED [ 68%]
50
- tests/tests_for_work_items_service.py::test_graphql_raises_on_errors PASSED [ 70%]
51
- tests/tests_for_work_items_service.py::test_paginate_graphql_follows_end_cursor PASSED [ 72%]
52
- tests/tests_for_work_items_service.py::test_get_work_item_by_global_id PASSED [ 74%]
53
- tests/tests_for_work_items_service.py::test_get_work_item_by_project_path_and_iid_uses_namespace_lookup PASSED [ 75%]
54
- tests/tests_for_work_items_service.py::test_get_work_item_missing_raises PASSED [ 77%]
55
- tests/tests_for_work_items_service.py::test_search_work_items_paginates_and_translates_state PASSED [ 79%]
56
- tests/tests_for_work_items_service.py::test_search_work_items_group_scope_uses_group_field PASSED [ 81%]
57
- tests/tests_for_work_items_service.py::test_search_work_items_requires_one_scope PASSED [ 82%]
58
- tests/tests_for_work_items_service.py::test_set_work_item_state_closes_via_workItemUpdate PASSED [ 84%]
59
- tests/tests_for_work_items_service.py::test_set_work_item_state_mutation_errors_raise PASSED [ 86%]
60
- tests/tests_for_work_items_service.py::test_list_work_item_notes_filters_system_notes PASSED [ 87%]
61
- tests/tests_for_work_items_service.py::test_comment_on_work_item_creates_note PASSED [ 89%]
62
- tests/tests_for_work_items_service.py::test_reply_to_work_item_discussion_threads_via_discussion_id PASSED [ 91%]
14
+ tests/tests_for_api_utilities.py::test_paginate_consumes_x_next_page PASSED [ 7%]
15
+ tests/tests_for_api_utilities.py::test_async_retry_http_aborts_on_ignored_status PASSED [ 9%]
16
+ tests/tests_for_api_utilities.py::test_async_retry_http_retries_on_5xx PASSED [ 11%]
17
+ tests/tests_for_api_utilities.py::test_upload_to_project_returns_markdown PASSED [ 12%]
18
+ tests/tests_for_issues_service.py::test_search_issues_paginates PASSED [ 14%]
19
+ tests/tests_for_issues_service.py::test_create_issue_with_image_uploads_first_then_creates PASSED [ 15%]
20
+ tests/tests_for_issues_service.py::test_set_issue_state PASSED [ 17%]
21
+ tests/tests_for_issues_service.py::test_set_issue_labels_modes PASSED [ 19%]
22
+ tests/tests_for_issues_service.py::test_aggregate_issues_by_label PASSED [ 20%]
23
+ tests/tests_for_issues_service.py::test_comment_on_issue PASSED [ 22%]
24
+ tests/tests_for_merge_requests_service.py::test_get_merge_request PASSED [ 23%]
25
+ tests/tests_for_merge_requests_service.py::test_comment_on_merge_request PASSED [ 25%]
26
+ tests/tests_for_merge_requests_service.py::test_inline_diff_comment_payload_shape PASSED [ 26%]
27
+ tests/tests_for_merge_requests_service.py::test_approve_and_unapprove PASSED [ 28%]
28
+ tests/tests_for_models.py::test_config_api_base_url_strips_trailing_slash PASSED [ 30%]
29
+ tests/tests_for_models.py::test_issue_extra_fields_ignored PASSED [ 31%]
30
+ tests/tests_for_models.py::test_merge_request_basic PASSED [ 33%]
31
+ tests/tests_for_models.py::test_config_env_loading PASSED [ 34%]
32
+ tests/tests_for_oauth_cli.py::test_missing_client_id_errors PASSED [ 36%]
33
+ tests/tests_for_oauth_cli.py::test_success_prints_only_token PASSED [ 38%]
34
+ tests/tests_for_oauth_cli.py::test_reads_config_from_env PASSED [ 39%]
35
+ tests/tests_for_oauth_cli.py::test_bad_redirect_port_errors PASSED [ 41%]
36
+ tests/tests_for_oauth_cli.py::test_oauth_error_returns_one PASSED [ 42%]
37
+ tests/tests_for_oauth_service.py::test_make_pkce_challenge_is_s256_of_verifier PASSED [ 44%]
38
+ tests/tests_for_oauth_service.py::test_cache_path_is_per_host_and_private PASSED [ 46%]
39
+ tests/tests_for_oauth_service.py::test_token_if_valid_expiry PASSED [ 47%]
40
+ tests/tests_for_oauth_service.py::test_store_token_response_roundtrip PASSED [ 49%]
41
+ tests/tests_for_oauth_service.py::test_store_token_response_requires_access_token PASSED [ 50%]
42
+ tests/tests_for_oauth_service.py::test_refresh_access_token_success PASSED [ 52%]
43
+ tests/tests_for_oauth_service.py::test_refresh_access_token_returns_none_on_error PASSED [ 53%]
44
+ tests/tests_for_oauth_service.py::test_get_access_token_reuses_valid_cache PASSED [ 55%]
45
+ tests/tests_for_oauth_service.py::test_get_access_token_refreshes_expired_cache PASSED [ 57%]
46
+ tests/tests_for_oauth_service.py::test_get_access_token_requires_client_id PASSED [ 58%]
47
+ tests/tests_for_packages_service.py::test_upload_generic_package_url_shape PASSED [ 60%]
48
+ tests/tests_for_packages_service.py::test_download_generic_package PASSED [ 61%]
49
+ tests/tests_for_packages_service.py::test_list_packages PASSED [ 63%]
50
+ tests/tests_for_repo_files_service.py::test_get_file_url_encoded PASSED [ 65%]
51
+ tests/tests_for_repo_files_service.py::test_download_file_writes_to_dest PASSED [ 66%]
52
+ tests/tests_for_repo_files_service.py::test_download_directory_walks_tree PASSED [ 68%]
53
+ tests/tests_for_work_items_service.py::test_make_work_item_gid_passthrough_and_coerce PASSED [ 69%]
54
+ tests/tests_for_work_items_service.py::test_work_item_from_node_flattens_widgets PASSED [ 71%]
55
+ tests/tests_for_work_items_service.py::test_graphql_raises_on_errors PASSED [ 73%]
56
+ tests/tests_for_work_items_service.py::test_paginate_graphql_follows_end_cursor PASSED [ 74%]
57
+ tests/tests_for_work_items_service.py::test_get_work_item_by_global_id PASSED [ 76%]
58
+ tests/tests_for_work_items_service.py::test_get_work_item_by_project_path_and_iid_uses_namespace_lookup PASSED [ 77%]
59
+ tests/tests_for_work_items_service.py::test_get_work_item_missing_raises PASSED [ 79%]
60
+ tests/tests_for_work_items_service.py::test_search_work_items_paginates_and_translates_state PASSED [ 80%]
61
+ tests/tests_for_work_items_service.py::test_search_work_items_group_scope_uses_group_field PASSED [ 82%]
62
+ tests/tests_for_work_items_service.py::test_search_work_items_requires_one_scope PASSED [ 84%]
63
+ tests/tests_for_work_items_service.py::test_set_work_item_state_closes_via_workItemUpdate PASSED [ 85%]
64
+ tests/tests_for_work_items_service.py::test_set_work_item_state_mutation_errors_raise PASSED [ 87%]
65
+ tests/tests_for_work_items_service.py::test_list_work_item_notes_filters_system_notes PASSED [ 88%]
66
+ tests/tests_for_work_items_service.py::test_comment_on_work_item_creates_note PASSED [ 90%]
67
+ tests/tests_for_work_items_service.py::test_reply_to_work_item_discussion_threads_via_discussion_id PASSED [ 92%]
63
68
  tests/tests_for_work_items_service.py::test_set_work_item_labels_add_passes_addLabelIds PASSED [ 93%]
64
- tests/tests_for_work_items_service.py::test_set_work_item_labels_remove_passes_removeLabelIds PASSED [ 94%]
69
+ tests/tests_for_work_items_service.py::test_set_work_item_labels_remove_passes_removeLabelIds PASSED [ 95%]
65
70
  tests/tests_for_work_items_service.py::test_list_labels_for_project_includes_ancestor_groups PASSED [ 96%]
66
71
  tests/tests_for_work_items_service.py::test_create_label_passes_project_path_color_and_description PASSED [ 98%]
67
72
  tests/tests_for_work_items_service.py::test_create_label_group_scope_passes_group_path PASSED [100%]
68
73
 
69
- ============================== 58 passed in 1.76s ==============================
74
+ ============================== 63 passed in 1.91s ==============================
@@ -0,0 +1,85 @@
1
+ import sys
2
+
3
+ import pytest
4
+
5
+ from pjdev_gitlab import oauth_cli
6
+ from pjdev_gitlab.oauth_service import OAuthError
7
+
8
+
9
+ @pytest.fixture(autouse=True)
10
+ def _clear_oauth_env(monkeypatch):
11
+ for key in (
12
+ "GL_GITLAB_URL",
13
+ "GL_OAUTH_CLIENT_ID",
14
+ "GL_OAUTH_CLIENT_SECRET",
15
+ "GL_OAUTH_REDIRECT_PORT",
16
+ "GL_OAUTH_SCOPES",
17
+ ):
18
+ monkeypatch.delenv(key, raising=False)
19
+
20
+
21
+ def _run(monkeypatch, argv):
22
+ monkeypatch.setattr(sys, "argv", ["pjdev-gitlab-auth", *argv])
23
+ return oauth_cli.main()
24
+
25
+
26
+ def test_missing_client_id_errors(monkeypatch, capsys):
27
+ rc = _run(monkeypatch, ["--gitlab-url", "https://gitlab.example.com"])
28
+ assert rc == 2
29
+ out = capsys.readouterr()
30
+ assert out.out == "" # never print a partial/empty token
31
+ assert "client id" in out.err.lower()
32
+
33
+
34
+ def test_success_prints_only_token(monkeypatch, capsys):
35
+ captured = {}
36
+
37
+ def fake_get(**kwargs):
38
+ captured.update(kwargs)
39
+ return "TOK123"
40
+
41
+ monkeypatch.setattr(oauth_cli, "get_access_token", fake_get)
42
+ rc = _run(
43
+ monkeypatch,
44
+ ["--gitlab-url", "https://gitlab.example.com", "--client-id", "cid", "--force"],
45
+ )
46
+ assert rc == 0
47
+ out = capsys.readouterr()
48
+ assert out.out.strip() == "TOK123"
49
+ assert captured["gitlab_url"] == "https://gitlab.example.com"
50
+ assert captured["client_id"] == "cid"
51
+ assert captured["force"] is True
52
+ assert captured["scopes"] == "api"
53
+
54
+
55
+ def test_reads_config_from_env(monkeypatch, capsys):
56
+ monkeypatch.setenv("GL_GITLAB_URL", "https://env.example.com")
57
+ monkeypatch.setenv("GL_OAUTH_CLIENT_ID", "env-cid")
58
+ captured = {}
59
+ monkeypatch.setattr(
60
+ oauth_cli, "get_access_token", lambda **k: captured.update(k) or "ENVTOK"
61
+ )
62
+ rc = _run(monkeypatch, [])
63
+ assert rc == 0
64
+ assert capsys.readouterr().out.strip() == "ENVTOK"
65
+ assert captured["gitlab_url"] == "https://env.example.com"
66
+ assert captured["client_id"] == "env-cid"
67
+
68
+
69
+ def test_bad_redirect_port_errors(monkeypatch):
70
+ # argparse rejects a non-int --redirect-port with SystemExit(2).
71
+ with pytest.raises(SystemExit) as exc:
72
+ _run(monkeypatch, ["--client-id", "cid", "--redirect-port", "notaport"])
73
+ assert exc.value.code == 2
74
+
75
+
76
+ def test_oauth_error_returns_one(monkeypatch, capsys):
77
+ def boom(**_kwargs):
78
+ raise OAuthError("nope")
79
+
80
+ monkeypatch.setattr(oauth_cli, "get_access_token", boom)
81
+ rc = _run(monkeypatch, ["--client-id", "cid"])
82
+ assert rc == 1
83
+ out = capsys.readouterr()
84
+ assert out.out == ""
85
+ assert "nope" in out.err
File without changes
File without changes
File without changes