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.
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/PKG-INFO +18 -1
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/README.md +17 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/pyproject.toml +3 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/__about__.py +1 -1
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/__init__.py +1 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/api_utilities.py +1 -1
- pjdev_gitlab-5.1.10/src/pjdev_gitlab/oauth_cli.py +122 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/test_report_pjdev-gitlab.txt +58 -53
- pjdev_gitlab-5.1.10/tests/tests_for_oauth_cli.py +85 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/.gitignore +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/LICENSE.txt +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_issues/SKILL.md +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_merge_requests/SKILL.md +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_packages/SKILL.md +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_repo_files/SKILL.md +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/.agents/skills/pjdev_gitlab_work_items/SKILL.md +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/config_service.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/issues_service.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/merge_requests_service.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/models.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/oauth_service.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/packages_service.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/py.typed +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/repo_files_service.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/src/pjdev_gitlab/work_items_service.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/test.sh +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/__init__.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/conftest.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_api_utilities.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_issues_service.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_merge_requests_service.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_models.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_oauth_service.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_packages_service.py +0 -0
- {pjdev_gitlab-5.1.9 → pjdev_gitlab-5.1.10}/tests/tests_for_repo_files_service.py +0 -0
- {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.
|
|
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
|
|
@@ -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,
|
|
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
|
|
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 [
|
|
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 [
|
|
15
|
-
tests/tests_for_api_utilities.py::test_async_retry_http_aborts_on_ignored_status PASSED [
|
|
16
|
-
tests/tests_for_api_utilities.py::test_async_retry_http_retries_on_5xx PASSED [
|
|
17
|
-
tests/tests_for_api_utilities.py::test_upload_to_project_returns_markdown PASSED [
|
|
18
|
-
tests/tests_for_issues_service.py::test_search_issues_paginates PASSED [
|
|
19
|
-
tests/tests_for_issues_service.py::test_create_issue_with_image_uploads_first_then_creates PASSED [
|
|
20
|
-
tests/tests_for_issues_service.py::test_set_issue_state PASSED [
|
|
21
|
-
tests/tests_for_issues_service.py::test_set_issue_labels_modes PASSED [
|
|
22
|
-
tests/tests_for_issues_service.py::test_aggregate_issues_by_label PASSED [
|
|
23
|
-
tests/tests_for_issues_service.py::test_comment_on_issue PASSED [
|
|
24
|
-
tests/tests_for_merge_requests_service.py::test_get_merge_request PASSED [
|
|
25
|
-
tests/tests_for_merge_requests_service.py::test_comment_on_merge_request PASSED [
|
|
26
|
-
tests/tests_for_merge_requests_service.py::test_inline_diff_comment_payload_shape PASSED [
|
|
27
|
-
tests/tests_for_merge_requests_service.py::test_approve_and_unapprove PASSED [
|
|
28
|
-
tests/tests_for_models.py::test_config_api_base_url_strips_trailing_slash PASSED [
|
|
29
|
-
tests/tests_for_models.py::test_issue_extra_fields_ignored PASSED [
|
|
30
|
-
tests/tests_for_models.py::test_merge_request_basic PASSED [
|
|
31
|
-
tests/tests_for_models.py::test_config_env_loading PASSED [
|
|
32
|
-
tests/
|
|
33
|
-
tests/
|
|
34
|
-
tests/
|
|
35
|
-
tests/
|
|
36
|
-
tests/
|
|
37
|
-
tests/tests_for_oauth_service.py::
|
|
38
|
-
tests/tests_for_oauth_service.py::
|
|
39
|
-
tests/tests_for_oauth_service.py::
|
|
40
|
-
tests/tests_for_oauth_service.py::
|
|
41
|
-
tests/tests_for_oauth_service.py::
|
|
42
|
-
tests/
|
|
43
|
-
tests/
|
|
44
|
-
tests/
|
|
45
|
-
tests/
|
|
46
|
-
tests/
|
|
47
|
-
tests/
|
|
48
|
-
tests/
|
|
49
|
-
tests/
|
|
50
|
-
tests/
|
|
51
|
-
tests/
|
|
52
|
-
tests/
|
|
53
|
-
tests/tests_for_work_items_service.py::
|
|
54
|
-
tests/tests_for_work_items_service.py::
|
|
55
|
-
tests/tests_for_work_items_service.py::
|
|
56
|
-
tests/tests_for_work_items_service.py::
|
|
57
|
-
tests/tests_for_work_items_service.py::
|
|
58
|
-
tests/tests_for_work_items_service.py::
|
|
59
|
-
tests/tests_for_work_items_service.py::
|
|
60
|
-
tests/tests_for_work_items_service.py::
|
|
61
|
-
tests/tests_for_work_items_service.py::
|
|
62
|
-
tests/tests_for_work_items_service.py::
|
|
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 [
|
|
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
|
-
==============================
|
|
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
|
|
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
|