youtrack-cli 0.3.0__tar.gz → 0.3.2__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.
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.claude/settings.local.json +2 -1
- youtrack_cli-0.3.2/CLAUDE.md +35 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/PKG-INFO +1 -1
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/justfile +2 -2
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/pyproject.toml +2 -2
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_admin.py +22 -64
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_articles.py +15 -45
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_auth.py +7 -20
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_boards.py +7 -21
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_issues.py +31 -93
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_logging.py +2 -10
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_main.py +51 -28
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_projects.py +16 -48
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_reports.py +6 -18
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_security.py +3 -9
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_time.py +5 -15
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_users.py +32 -96
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/uv.lock +1 -1
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/admin.py +15 -31
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/articles.py +6 -17
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/auth.py +4 -11
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/boards.py +5 -16
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/cache.py +5 -15
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/cli_utils/aliases.py +4 -4
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/client.py +8 -25
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/articles.py +9 -25
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/issues.py +16 -47
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/projects.py +4 -13
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/time_tracking.py +2 -6
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/users.py +3 -9
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/common.py +2 -6
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/exceptions.py +4 -12
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/issues.py +21 -69
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/logging.py +3 -9
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/main.py +19 -60
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/performance.py +3 -11
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/progress.py +1 -3
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/projects.py +6 -19
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/reports.py +29 -79
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/security.py +9 -27
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/time.py +4 -14
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/users.py +6 -20
- youtrack_cli-0.3.0/CLAUDE.md +0 -204
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.github/dependabot.yml +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.github/workflows/ci.yml +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.github/workflows/release.yml +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.gitignore +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.pre-commit-config.yaml +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/.readthedocs.yaml +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/PUBLISHING.md +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/README.md +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/Makefile +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/api/index.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/changelog.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/command-aliases.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/admin.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/articles.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/auth.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/boards.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/config.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/index.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/issues.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/projects.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/reports.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/time.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/commands/users.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/conf.py +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/configuration.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/development.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/index.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/installation.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/learning-path.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/logging.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/performance.md +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/progress-indicators.md +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/quickstart.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/requirements.txt +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/security.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/troubleshooting.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/workflows.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/docs/youtrack-concepts.rst +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/package-lock.json +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/package.json +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/__init__.py +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/conftest.py +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tests/test_config.py +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/tox.ini +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/__init__.py +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/cli_utils/__init__.py +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/__init__.py +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/boards.py +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/commands/common.py +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/config.py +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/py.typed +0 -0
- {youtrack_cli-0.3.0 → youtrack_cli-0.3.2}/youtrack_cli/utils.py +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Plan
|
|
6
|
+
|
|
7
|
+
This is a YouTrack CLI application for interacting with JetBrains YouTrack issue tracking system via command line interface. This cli will offer an ergonomic, best practice cli and will leverage
|
|
8
|
+
|
|
9
|
+
- rich
|
|
10
|
+
- textual
|
|
11
|
+
- pydantic
|
|
12
|
+
|
|
13
|
+
We use `uv` for managing dependencies.
|
|
14
|
+
|
|
15
|
+
## Create
|
|
16
|
+
|
|
17
|
+
Each new feature must have a corresponding github issue. When working on a new issue a new feature branch must be created with the name of the branch matching the name of the issue with the issue number in it.
|
|
18
|
+
|
|
19
|
+
For every change that is implemented, the README.md file MUST be updated to reflect that change.
|
|
20
|
+
|
|
21
|
+
## Test
|
|
22
|
+
|
|
23
|
+
All tests must pass. We use `pytest` for testing, `ruff` for linting, `ty` for type checking, `tox` for running on various versions of Python. We'll utilize `zizmor` for reviewing our GitHub Actions. Pre-commit hooks ensure code quality before commits. All commands should be run with uv.
|
|
24
|
+
|
|
25
|
+
## Documentation
|
|
26
|
+
|
|
27
|
+
Documentation is available in the docs/ folder. Any new functionality should have documentation written for it there. The README.md file shoudl not be used for comprehensive documentation.
|
|
28
|
+
|
|
29
|
+
## Deploy
|
|
30
|
+
|
|
31
|
+
Deployment will always be done to a feature branch. When a feature is significant enough, we'll bump the version of the tool and tag it with that version. We will have a github action that deploys this to Test PyPI and PyPI using a `release.yml` GitHub Action.
|
|
32
|
+
|
|
33
|
+
## Current Configuration
|
|
34
|
+
|
|
35
|
+
- Claude Code permissions are configured in `.claude/settings.local.json`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: youtrack-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: YouTrack CLI - Command line interface for JetBrains YouTrack issue tracking system
|
|
5
5
|
Project-URL: Homepage, https://github.com/ryan-murphy/yt-cli
|
|
6
6
|
Project-URL: Documentation, https://yt-cli.readthedocs.io/
|
|
@@ -52,8 +52,8 @@ format-check:
|
|
|
52
52
|
[group('quality')]
|
|
53
53
|
typecheck:
|
|
54
54
|
#!/usr/bin/env bash
|
|
55
|
-
echo "Running
|
|
56
|
-
uv run
|
|
55
|
+
echo "Running ty type checker..."
|
|
56
|
+
uv run ty youtrack_cli
|
|
57
57
|
echo "✅ Type checking complete"
|
|
58
58
|
|
|
59
59
|
[group('quality')]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "youtrack-cli"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.2"
|
|
4
4
|
description = "YouTrack CLI - Command line interface for JetBrains YouTrack issue tracking system"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.9, <3.14"
|
|
@@ -62,7 +62,7 @@ packages = ["youtrack_cli"]
|
|
|
62
62
|
|
|
63
63
|
[tool.ruff]
|
|
64
64
|
target-version = "py39"
|
|
65
|
-
line-length =
|
|
65
|
+
line-length = 120
|
|
66
66
|
|
|
67
67
|
[tool.ruff.lint]
|
|
68
68
|
select = [
|
|
@@ -51,9 +51,7 @@ class TestAdminManager:
|
|
|
51
51
|
mock_response.json.return_value = mock_settings
|
|
52
52
|
mock_response.raise_for_status.return_value = None
|
|
53
53
|
|
|
54
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
55
|
-
mock_response
|
|
56
|
-
)
|
|
54
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
|
|
57
55
|
|
|
58
56
|
result = await admin_manager.get_global_settings()
|
|
59
57
|
|
|
@@ -71,20 +69,14 @@ class TestAdminManager:
|
|
|
71
69
|
assert "Not authenticated" in result["message"]
|
|
72
70
|
|
|
73
71
|
@pytest.mark.asyncio
|
|
74
|
-
async def test_get_global_settings_insufficient_permissions(
|
|
75
|
-
self, admin_manager, auth_manager
|
|
76
|
-
):
|
|
72
|
+
async def test_get_global_settings_insufficient_permissions(self, admin_manager, auth_manager):
|
|
77
73
|
"""Test global settings retrieval with insufficient permissions."""
|
|
78
74
|
with patch("httpx.AsyncClient") as mock_client:
|
|
79
75
|
mock_response = Mock()
|
|
80
76
|
mock_response.status_code = 403
|
|
81
77
|
mock_request = Mock()
|
|
82
|
-
http_error = httpx.HTTPStatusError(
|
|
83
|
-
|
|
84
|
-
)
|
|
85
|
-
mock_client.return_value.__aenter__.return_value.get.side_effect = (
|
|
86
|
-
http_error
|
|
87
|
-
)
|
|
78
|
+
http_error = httpx.HTTPStatusError("Forbidden", request=mock_request, response=mock_response)
|
|
79
|
+
mock_client.return_value.__aenter__.return_value.get.side_effect = http_error
|
|
88
80
|
|
|
89
81
|
result = await admin_manager.get_global_settings()
|
|
90
82
|
|
|
@@ -98,9 +90,7 @@ class TestAdminManager:
|
|
|
98
90
|
mock_response = Mock()
|
|
99
91
|
mock_response.raise_for_status.return_value = None
|
|
100
92
|
|
|
101
|
-
mock_client.return_value.__aenter__.return_value.post.return_value =
|
|
102
|
-
mock_response
|
|
103
|
-
)
|
|
93
|
+
mock_client.return_value.__aenter__.return_value.post.return_value = mock_response
|
|
104
94
|
|
|
105
95
|
result = await admin_manager.set_global_setting("server.name", "New Name")
|
|
106
96
|
|
|
@@ -114,12 +104,8 @@ class TestAdminManager:
|
|
|
114
104
|
mock_response = Mock()
|
|
115
105
|
mock_response.status_code = 400
|
|
116
106
|
mock_request = Mock()
|
|
117
|
-
http_error = httpx.HTTPStatusError(
|
|
118
|
-
|
|
119
|
-
)
|
|
120
|
-
mock_client.return_value.__aenter__.return_value.post.side_effect = (
|
|
121
|
-
http_error
|
|
122
|
-
)
|
|
107
|
+
http_error = httpx.HTTPStatusError("Bad Request", request=mock_request, response=mock_response)
|
|
108
|
+
mock_client.return_value.__aenter__.return_value.post.side_effect = http_error
|
|
123
109
|
|
|
124
110
|
result = await admin_manager.set_global_setting("invalid.key", "value")
|
|
125
111
|
|
|
@@ -142,9 +128,7 @@ class TestAdminManager:
|
|
|
142
128
|
mock_response.json.return_value = mock_license
|
|
143
129
|
mock_response.raise_for_status.return_value = None
|
|
144
130
|
|
|
145
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
146
|
-
mock_response
|
|
147
|
-
)
|
|
131
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
|
|
148
132
|
|
|
149
133
|
result = await admin_manager.get_license_info()
|
|
150
134
|
|
|
@@ -161,9 +145,7 @@ class TestAdminManager:
|
|
|
161
145
|
mock_response.json.return_value = mock_usage
|
|
162
146
|
mock_response.raise_for_status.return_value = None
|
|
163
147
|
|
|
164
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
165
|
-
mock_response
|
|
166
|
-
)
|
|
148
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
|
|
167
149
|
|
|
168
150
|
result = await admin_manager.get_license_usage()
|
|
169
151
|
|
|
@@ -186,9 +168,7 @@ class TestAdminManager:
|
|
|
186
168
|
mock_response.json.return_value = mock_health
|
|
187
169
|
mock_response.raise_for_status.return_value = None
|
|
188
170
|
|
|
189
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
190
|
-
mock_response
|
|
191
|
-
)
|
|
171
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
|
|
192
172
|
|
|
193
173
|
result = await admin_manager.get_system_health()
|
|
194
174
|
|
|
@@ -202,9 +182,7 @@ class TestAdminManager:
|
|
|
202
182
|
mock_response = Mock()
|
|
203
183
|
mock_response.raise_for_status.return_value = None
|
|
204
184
|
|
|
205
|
-
mock_client.return_value.__aenter__.return_value.post.return_value =
|
|
206
|
-
mock_response
|
|
207
|
-
)
|
|
185
|
+
mock_client.return_value.__aenter__.return_value.post.return_value = mock_response
|
|
208
186
|
|
|
209
187
|
result = await admin_manager.clear_caches()
|
|
210
188
|
|
|
@@ -231,12 +209,10 @@ class TestAdminManager:
|
|
|
231
209
|
|
|
232
210
|
with patch("httpx.AsyncClient") as mock_client:
|
|
233
211
|
mock_response = Mock()
|
|
234
|
-
mock_response.json.return_value = mock_groups
|
|
212
|
+
mock_response.json.return_value = {"usergroups": mock_groups}
|
|
235
213
|
mock_response.raise_for_status.return_value = None
|
|
236
214
|
|
|
237
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
238
|
-
mock_response
|
|
239
|
-
)
|
|
215
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
|
|
240
216
|
|
|
241
217
|
result = await admin_manager.list_user_groups()
|
|
242
218
|
|
|
@@ -257,9 +233,7 @@ class TestAdminManager:
|
|
|
257
233
|
mock_response.json.return_value = mock_created_group
|
|
258
234
|
mock_response.raise_for_status.return_value = None
|
|
259
235
|
|
|
260
|
-
mock_client.return_value.__aenter__.return_value.post.return_value =
|
|
261
|
-
mock_response
|
|
262
|
-
)
|
|
236
|
+
mock_client.return_value.__aenter__.return_value.post.return_value = mock_response
|
|
263
237
|
|
|
264
238
|
result = await admin_manager.create_user_group("New Group", "A new group")
|
|
265
239
|
|
|
@@ -274,12 +248,8 @@ class TestAdminManager:
|
|
|
274
248
|
mock_response = Mock()
|
|
275
249
|
mock_response.status_code = 400
|
|
276
250
|
mock_request = Mock()
|
|
277
|
-
http_error = httpx.HTTPStatusError(
|
|
278
|
-
|
|
279
|
-
)
|
|
280
|
-
mock_client.return_value.__aenter__.return_value.post.side_effect = (
|
|
281
|
-
http_error
|
|
282
|
-
)
|
|
251
|
+
http_error = httpx.HTTPStatusError("Bad Request", request=mock_request, response=mock_response)
|
|
252
|
+
mock_client.return_value.__aenter__.return_value.post.side_effect = http_error
|
|
283
253
|
|
|
284
254
|
result = await admin_manager.create_user_group("Existing Group")
|
|
285
255
|
|
|
@@ -311,9 +281,7 @@ class TestAdminManager:
|
|
|
311
281
|
mock_response.json.return_value = mock_fields
|
|
312
282
|
mock_response.raise_for_status.return_value = None
|
|
313
283
|
|
|
314
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
315
|
-
mock_response
|
|
316
|
-
)
|
|
284
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
|
|
317
285
|
|
|
318
286
|
result = await admin_manager.list_custom_fields()
|
|
319
287
|
|
|
@@ -387,9 +355,7 @@ class TestAdminManager:
|
|
|
387
355
|
# Verify that it shows "No user groups found"
|
|
388
356
|
mock_print.assert_called()
|
|
389
357
|
call_args = [call[0][0] for call in mock_print.call_args_list]
|
|
390
|
-
no_groups_found = any(
|
|
391
|
-
"No user groups found" in str(arg) for arg in call_args
|
|
392
|
-
)
|
|
358
|
+
no_groups_found = any("No user groups found" in str(arg) for arg in call_args)
|
|
393
359
|
assert no_groups_found
|
|
394
360
|
|
|
395
361
|
def test_display_user_groups_with_data(self, admin_manager):
|
|
@@ -417,9 +383,7 @@ class TestAdminManager:
|
|
|
417
383
|
# Verify that it shows "No custom fields found"
|
|
418
384
|
mock_print.assert_called()
|
|
419
385
|
call_args = [call[0][0] for call in mock_print.call_args_list]
|
|
420
|
-
no_fields_found = any(
|
|
421
|
-
"No custom fields found" in str(arg) for arg in call_args
|
|
422
|
-
)
|
|
386
|
+
no_fields_found = any("No custom fields found" in str(arg) for arg in call_args)
|
|
423
387
|
assert no_fields_found
|
|
424
388
|
|
|
425
389
|
def test_display_custom_fields_with_data(self, admin_manager):
|
|
@@ -478,9 +442,7 @@ class TestAdminCommands:
|
|
|
478
442
|
}
|
|
479
443
|
|
|
480
444
|
with patch("asyncio.run") as mock_asyncio:
|
|
481
|
-
result = self.runner.invoke(
|
|
482
|
-
main, ["admin", "global-settings", "set", "server.name", "New Name"]
|
|
483
|
-
)
|
|
445
|
+
result = self.runner.invoke(main, ["admin", "global-settings", "set", "server.name", "New Name"])
|
|
484
446
|
|
|
485
447
|
assert result.exit_code == 0
|
|
486
448
|
mock_asyncio.assert_called_once()
|
|
@@ -525,9 +487,7 @@ class TestAdminCommands:
|
|
|
525
487
|
}
|
|
526
488
|
|
|
527
489
|
with patch("asyncio.run") as mock_asyncio:
|
|
528
|
-
result = self.runner.invoke(
|
|
529
|
-
main, ["admin", "maintenance", "clear-cache", "--confirm"]
|
|
530
|
-
)
|
|
490
|
+
result = self.runner.invoke(main, ["admin", "maintenance", "clear-cache", "--confirm"])
|
|
531
491
|
|
|
532
492
|
assert result.exit_code == 0
|
|
533
493
|
mock_asyncio.assert_called_once()
|
|
@@ -572,9 +532,7 @@ class TestAdminCommands:
|
|
|
572
532
|
}
|
|
573
533
|
|
|
574
534
|
with patch("asyncio.run") as mock_asyncio:
|
|
575
|
-
result = self.runner.invoke(
|
|
576
|
-
main, ["admin", "user-groups", "create", "NewGroup"]
|
|
577
|
-
)
|
|
535
|
+
result = self.runner.invoke(main, ["admin", "user-groups", "create", "NewGroup"])
|
|
578
536
|
|
|
579
537
|
assert result.exit_code == 0
|
|
580
538
|
mock_asyncio.assert_called_once()
|
|
@@ -52,9 +52,7 @@ class TestArticleManager:
|
|
|
52
52
|
mock_resp.text = '{"id": "123", "summary": "Test Article"}'
|
|
53
53
|
mock_resp.headers = {"content-type": "application/json"}
|
|
54
54
|
mock_resp.raise_for_status.return_value = None
|
|
55
|
-
mock_client.return_value.__aenter__.return_value.post.return_value =
|
|
56
|
-
mock_resp # noqa: E501
|
|
57
|
-
)
|
|
55
|
+
mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
|
|
58
56
|
|
|
59
57
|
result = await article_manager.create_article(
|
|
60
58
|
title="Test Article",
|
|
@@ -72,9 +70,7 @@ class TestArticleManager:
|
|
|
72
70
|
mock_resp = Mock()
|
|
73
71
|
mock_resp.status_code = 400
|
|
74
72
|
mock_resp.text = "Bad Request"
|
|
75
|
-
mock_client.return_value.__aenter__.return_value.post.return_value =
|
|
76
|
-
mock_resp # noqa: E501
|
|
77
|
-
)
|
|
73
|
+
mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
|
|
78
74
|
|
|
79
75
|
result = await article_manager.create_article(
|
|
80
76
|
title="Test Article",
|
|
@@ -107,9 +103,7 @@ class TestArticleManager:
|
|
|
107
103
|
mock_resp.text = '[{"id": "123", "summary": "Article 1"}]'
|
|
108
104
|
mock_resp.headers = {"content-type": "application/json"}
|
|
109
105
|
mock_resp.raise_for_status.return_value = None
|
|
110
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
111
|
-
mock_resp # noqa: E501
|
|
112
|
-
)
|
|
106
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
|
|
113
107
|
|
|
114
108
|
result = await article_manager.list_articles()
|
|
115
109
|
|
|
@@ -133,9 +127,7 @@ class TestArticleManager:
|
|
|
133
127
|
mock_resp.text = '{"mock": "response"}'
|
|
134
128
|
mock_resp.headers = {"content-type": "application/json"}
|
|
135
129
|
mock_resp.raise_for_status.return_value = None
|
|
136
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
137
|
-
mock_resp # noqa: E501
|
|
138
|
-
)
|
|
130
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
|
|
139
131
|
|
|
140
132
|
result = await article_manager.get_article("123")
|
|
141
133
|
|
|
@@ -158,9 +150,7 @@ class TestArticleManager:
|
|
|
158
150
|
mock_resp.text = '{"mock": "response"}'
|
|
159
151
|
mock_resp.headers = {"content-type": "application/json"}
|
|
160
152
|
mock_resp.raise_for_status.return_value = None
|
|
161
|
-
mock_client.return_value.__aenter__.return_value.post.return_value =
|
|
162
|
-
mock_resp # noqa: E501
|
|
163
|
-
)
|
|
153
|
+
mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
|
|
164
154
|
|
|
165
155
|
result = await article_manager.update_article(
|
|
166
156
|
article_id="123",
|
|
@@ -187,9 +177,7 @@ class TestArticleManager:
|
|
|
187
177
|
mock_resp = Mock()
|
|
188
178
|
mock_resp.status_code = 200
|
|
189
179
|
mock_resp.raise_for_status.return_value = None
|
|
190
|
-
mock_client.return_value.__aenter__.return_value.delete.return_value =
|
|
191
|
-
mock_resp # noqa: E501
|
|
192
|
-
)
|
|
180
|
+
mock_client.return_value.__aenter__.return_value.delete.return_value = mock_resp # noqa: E501
|
|
193
181
|
|
|
194
182
|
result = await article_manager.delete_article("123")
|
|
195
183
|
|
|
@@ -212,9 +200,7 @@ class TestArticleManager:
|
|
|
212
200
|
mock_resp.text = '{"mock": "response"}'
|
|
213
201
|
mock_resp.headers = {"content-type": "application/json"}
|
|
214
202
|
mock_resp.raise_for_status.return_value = None
|
|
215
|
-
mock_client.return_value.__aenter__.return_value.post.return_value =
|
|
216
|
-
mock_resp # noqa: E501
|
|
217
|
-
)
|
|
203
|
+
mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
|
|
218
204
|
|
|
219
205
|
result = await article_manager.publish_article("123")
|
|
220
206
|
|
|
@@ -240,9 +226,7 @@ class TestArticleManager:
|
|
|
240
226
|
mock_resp.text = '{"mock": "response"}'
|
|
241
227
|
mock_resp.headers = {"content-type": "application/json"}
|
|
242
228
|
mock_resp.raise_for_status.return_value = None
|
|
243
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
244
|
-
mock_resp # noqa: E501
|
|
245
|
-
)
|
|
229
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
|
|
246
230
|
|
|
247
231
|
result = await article_manager.search_articles("search query")
|
|
248
232
|
|
|
@@ -268,9 +252,7 @@ class TestArticleManager:
|
|
|
268
252
|
mock_resp.text = '{"mock": "response"}'
|
|
269
253
|
mock_resp.headers = {"content-type": "application/json"}
|
|
270
254
|
mock_resp.raise_for_status.return_value = None
|
|
271
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
272
|
-
mock_resp # noqa: E501
|
|
273
|
-
)
|
|
255
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
|
|
274
256
|
|
|
275
257
|
result = await article_manager.get_article_comments("123")
|
|
276
258
|
|
|
@@ -293,9 +275,7 @@ class TestArticleManager:
|
|
|
293
275
|
mock_resp.text = '{"mock": "response"}'
|
|
294
276
|
mock_resp.headers = {"content-type": "application/json"}
|
|
295
277
|
mock_resp.raise_for_status.return_value = None
|
|
296
|
-
mock_client.return_value.__aenter__.return_value.post.return_value =
|
|
297
|
-
mock_resp # noqa: E501
|
|
298
|
-
)
|
|
278
|
+
mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
|
|
299
279
|
|
|
300
280
|
result = await article_manager.add_comment("123", "Test comment")
|
|
301
281
|
|
|
@@ -322,9 +302,7 @@ class TestArticleManager:
|
|
|
322
302
|
mock_resp.text = '{"mock": "response"}'
|
|
323
303
|
mock_resp.headers = {"content-type": "application/json"}
|
|
324
304
|
mock_resp.raise_for_status.return_value = None
|
|
325
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
326
|
-
mock_resp # noqa: E501
|
|
327
|
-
)
|
|
305
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
|
|
328
306
|
|
|
329
307
|
result = await article_manager.get_article_attachments("123")
|
|
330
308
|
|
|
@@ -350,9 +328,7 @@ class TestArticleManager:
|
|
|
350
328
|
article_manager.console = mock_console.return_value
|
|
351
329
|
article_manager.display_articles_table([])
|
|
352
330
|
|
|
353
|
-
mock_console.return_value.print.assert_called_with(
|
|
354
|
-
"No articles found.", style="yellow"
|
|
355
|
-
)
|
|
331
|
+
mock_console.return_value.print.assert_called_with("No articles found.", style="yellow")
|
|
356
332
|
|
|
357
333
|
def test_display_articles_table_with_data(self, article_manager):
|
|
358
334
|
"""Test displaying articles table with data."""
|
|
@@ -383,9 +359,7 @@ class TestArticleManager:
|
|
|
383
359
|
article_manager.console = mock_console.return_value
|
|
384
360
|
article_manager.display_articles_tree([])
|
|
385
361
|
|
|
386
|
-
mock_console.return_value.print.assert_called_with(
|
|
387
|
-
"No articles found.", style="yellow"
|
|
388
|
-
)
|
|
362
|
+
mock_console.return_value.print.assert_called_with("No articles found.", style="yellow")
|
|
389
363
|
|
|
390
364
|
def test_display_articles_tree_with_data(self, article_manager):
|
|
391
365
|
"""Test displaying articles tree with data."""
|
|
@@ -455,9 +429,7 @@ class TestArticlesCLI:
|
|
|
455
429
|
"data": {"id": "123"},
|
|
456
430
|
}
|
|
457
431
|
|
|
458
|
-
result = runner.invoke(
|
|
459
|
-
main, ["articles", "create", "Test Title", "--content", "Test content"]
|
|
460
|
-
)
|
|
432
|
+
result = runner.invoke(main, ["articles", "create", "Test Title", "--content", "Test content"])
|
|
461
433
|
|
|
462
434
|
assert result.exit_code == 0
|
|
463
435
|
assert "Creating article" in result.output
|
|
@@ -545,9 +517,7 @@ class TestArticlesCLI:
|
|
|
545
517
|
"data": {"id": "comment-1"},
|
|
546
518
|
}
|
|
547
519
|
|
|
548
|
-
result = runner.invoke(
|
|
549
|
-
main, ["articles", "comments", "add", "123", "Test comment"]
|
|
550
|
-
)
|
|
520
|
+
result = runner.invoke(main, ["articles", "comments", "add", "123", "Test comment"])
|
|
551
521
|
|
|
552
522
|
assert result.exit_code == 0
|
|
553
523
|
assert "Adding comment" in result.output
|
|
@@ -27,9 +27,7 @@ class TestAuthConfig:
|
|
|
27
27
|
|
|
28
28
|
def test_config_without_username(self):
|
|
29
29
|
"""Test creating config without username."""
|
|
30
|
-
config = AuthConfig(
|
|
31
|
-
base_url="https://example.youtrack.cloud", token="test-token-123"
|
|
32
|
-
)
|
|
30
|
+
config = AuthConfig(base_url="https://example.youtrack.cloud", token="test-token-123")
|
|
33
31
|
assert config.base_url == "https://example.youtrack.cloud"
|
|
34
32
|
assert config.token == "test-token-123"
|
|
35
33
|
assert config.username is None
|
|
@@ -51,8 +49,7 @@ class TestAuthManager:
|
|
|
51
49
|
|
|
52
50
|
# Store original environment variables to restore later
|
|
53
51
|
self.original_env = {
|
|
54
|
-
key: os.environ.get(key)
|
|
55
|
-
for key in ["YOUTRACK_BASE_URL", "YOUTRACK_TOKEN", "YOUTRACK_USERNAME"]
|
|
52
|
+
key: os.environ.get(key) for key in ["YOUTRACK_BASE_URL", "YOUTRACK_TOKEN", "YOUTRACK_USERNAME"]
|
|
56
53
|
}
|
|
57
54
|
|
|
58
55
|
def teardown_method(self):
|
|
@@ -93,9 +90,7 @@ class TestAuthManager:
|
|
|
93
90
|
def test_save_credentials_without_username(self):
|
|
94
91
|
"""Test saving credentials without username."""
|
|
95
92
|
# Force file storage instead of keyring for this test
|
|
96
|
-
self.auth_manager.save_credentials(
|
|
97
|
-
"https://example.youtrack.cloud", "test-token-123", use_keyring=False
|
|
98
|
-
)
|
|
93
|
+
self.auth_manager.save_credentials("https://example.youtrack.cloud", "test-token-123", use_keyring=False)
|
|
99
94
|
|
|
100
95
|
with open(self.config_path) as f:
|
|
101
96
|
content = f.read()
|
|
@@ -183,13 +178,9 @@ class TestAuthManager:
|
|
|
183
178
|
mock_response.raise_for_status.return_value = None
|
|
184
179
|
|
|
185
180
|
with patch("httpx.AsyncClient") as mock_client:
|
|
186
|
-
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
187
|
-
return_value=mock_response
|
|
188
|
-
)
|
|
181
|
+
mock_client.return_value.__aenter__.return_value.get = AsyncMock(return_value=mock_response)
|
|
189
182
|
|
|
190
|
-
result = await self.auth_manager.verify_credentials(
|
|
191
|
-
"https://example.youtrack.cloud", "test-token-123"
|
|
192
|
-
)
|
|
183
|
+
result = await self.auth_manager.verify_credentials("https://example.youtrack.cloud", "test-token-123")
|
|
193
184
|
|
|
194
185
|
assert result["status"] == "success"
|
|
195
186
|
assert result["username"] == "testuser"
|
|
@@ -200,13 +191,9 @@ class TestAuthManager:
|
|
|
200
191
|
async def test_verify_credentials_failure(self):
|
|
201
192
|
"""Test failed credential verification."""
|
|
202
193
|
with patch("httpx.AsyncClient") as mock_client:
|
|
203
|
-
mock_client.return_value.__aenter__.return_value.get = AsyncMock(
|
|
204
|
-
side_effect=Exception("HTTP Error")
|
|
205
|
-
)
|
|
194
|
+
mock_client.return_value.__aenter__.return_value.get = AsyncMock(side_effect=Exception("HTTP Error"))
|
|
206
195
|
|
|
207
|
-
result = await self.auth_manager.verify_credentials(
|
|
208
|
-
"https://example.youtrack.cloud", "invalid-token"
|
|
209
|
-
)
|
|
196
|
+
result = await self.auth_manager.verify_credentials("https://example.youtrack.cloud", "invalid-token")
|
|
210
197
|
|
|
211
198
|
assert result["status"] == "error"
|
|
212
199
|
assert "HTTP Error" in result["message"]
|
|
@@ -55,9 +55,7 @@ class TestBoardManager:
|
|
|
55
55
|
mock_resp.headers = {"content-type": "application/json"}
|
|
56
56
|
mock_resp.raise_for_status.return_value = None
|
|
57
57
|
|
|
58
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
59
|
-
mock_resp
|
|
60
|
-
)
|
|
58
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp
|
|
61
59
|
|
|
62
60
|
result = await board_manager.list_boards()
|
|
63
61
|
|
|
@@ -84,9 +82,7 @@ class TestBoardManager:
|
|
|
84
82
|
mock_resp.headers = {"content-type": "application/json"}
|
|
85
83
|
mock_resp.raise_for_status.return_value = None
|
|
86
84
|
|
|
87
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
88
|
-
mock_resp
|
|
89
|
-
)
|
|
85
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp
|
|
90
86
|
|
|
91
87
|
result = await board_manager.list_boards(project_id="TEST")
|
|
92
88
|
|
|
@@ -124,9 +120,7 @@ class TestBoardManager:
|
|
|
124
120
|
mock_resp.headers = {"content-type": "application/json"}
|
|
125
121
|
mock_resp.raise_for_status.return_value = None
|
|
126
122
|
|
|
127
|
-
mock_client.return_value.__aenter__.return_value.get.return_value =
|
|
128
|
-
mock_resp
|
|
129
|
-
)
|
|
123
|
+
mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp
|
|
130
124
|
|
|
131
125
|
result = await board_manager.view_board("123")
|
|
132
126
|
|
|
@@ -161,9 +155,7 @@ class TestBoardManager:
|
|
|
161
155
|
mock_resp.headers = {"content-type": "application/json"}
|
|
162
156
|
mock_resp.raise_for_status.return_value = None
|
|
163
157
|
|
|
164
|
-
mock_client.return_value.__aenter__.return_value.post.return_value =
|
|
165
|
-
mock_resp
|
|
166
|
-
)
|
|
158
|
+
mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp
|
|
167
159
|
|
|
168
160
|
result = await board_manager.update_board("123", name="Updated Board Name")
|
|
169
161
|
|
|
@@ -193,9 +185,7 @@ class TestBoardManager:
|
|
|
193
185
|
async def test_list_boards_general_error(self, board_manager):
|
|
194
186
|
"""Test board listing with general error."""
|
|
195
187
|
with patch("httpx.AsyncClient") as mock_client:
|
|
196
|
-
mock_client.return_value.__aenter__.return_value.get.side_effect = (
|
|
197
|
-
Exception("Connection error")
|
|
198
|
-
)
|
|
188
|
+
mock_client.return_value.__aenter__.return_value.get.side_effect = Exception("Connection error")
|
|
199
189
|
|
|
200
190
|
result = await board_manager.list_boards()
|
|
201
191
|
|
|
@@ -206,9 +196,7 @@ class TestBoardManager:
|
|
|
206
196
|
async def test_view_board_general_error(self, board_manager):
|
|
207
197
|
"""Test board viewing with general error."""
|
|
208
198
|
with patch("httpx.AsyncClient") as mock_client:
|
|
209
|
-
mock_client.return_value.__aenter__.return_value.get.side_effect = (
|
|
210
|
-
Exception("Connection error")
|
|
211
|
-
)
|
|
199
|
+
mock_client.return_value.__aenter__.return_value.get.side_effect = Exception("Connection error")
|
|
212
200
|
|
|
213
201
|
result = await board_manager.view_board("123")
|
|
214
202
|
|
|
@@ -219,9 +207,7 @@ class TestBoardManager:
|
|
|
219
207
|
async def test_update_board_general_error(self, board_manager):
|
|
220
208
|
"""Test board updating with general error."""
|
|
221
209
|
with patch("httpx.AsyncClient") as mock_client:
|
|
222
|
-
mock_client.return_value.__aenter__.return_value.post.side_effect = (
|
|
223
|
-
Exception("Connection error")
|
|
224
|
-
)
|
|
210
|
+
mock_client.return_value.__aenter__.return_value.post.side_effect = Exception("Connection error")
|
|
225
211
|
|
|
226
212
|
result = await board_manager.update_board("123", name="New Name")
|
|
227
213
|
|