youtrack-cli 0.3.7__tar.gz → 0.3.9__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 (94) hide show
  1. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/CLAUDE.md +11 -1
  2. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/PKG-INFO +1 -1
  3. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/justfile +1 -1
  4. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/pyproject.toml +1 -1
  5. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_admin.py +57 -38
  6. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_articles.py +45 -33
  7. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_boards.py +29 -19
  8. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_issues.py +109 -54
  9. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_logging.py +2 -0
  10. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_projects.py +40 -22
  11. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_reports.py +24 -11
  12. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_time.py +21 -13
  13. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_users.py +79 -72
  14. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/uv.lock +1 -1
  15. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/__init__.py +2 -0
  16. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/admin.py +252 -250
  17. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/articles.py +137 -136
  18. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/boards.py +48 -50
  19. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/client.py +3 -2
  20. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/issues.py +276 -274
  21. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/main.py +2 -1
  22. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/projects.py +113 -112
  23. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/reports.py +112 -120
  24. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/time.py +33 -31
  25. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/users.py +101 -164
  26. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/.claude/settings.local.json +0 -0
  27. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/.github/dependabot.yml +0 -0
  28. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/.github/workflows/ci.yml +0 -0
  29. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/.github/workflows/release.yml +0 -0
  30. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/.gitignore +0 -0
  31. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/.pre-commit-config.yaml +0 -0
  32. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/.readthedocs.yaml +0 -0
  33. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/PUBLISHING.md +0 -0
  34. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/README.md +0 -0
  35. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/Makefile +0 -0
  36. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/api/index.rst +0 -0
  37. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/changelog.rst +0 -0
  38. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/command-aliases.rst +0 -0
  39. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/commands/admin.rst +0 -0
  40. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/commands/articles.rst +0 -0
  41. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/commands/auth.rst +0 -0
  42. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/commands/boards.rst +0 -0
  43. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/commands/config.rst +0 -0
  44. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/commands/index.rst +0 -0
  45. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/commands/issues.rst +0 -0
  46. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/commands/projects.rst +0 -0
  47. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/commands/reports.rst +0 -0
  48. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/commands/time.rst +0 -0
  49. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/commands/users.rst +0 -0
  50. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/conf.py +0 -0
  51. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/configuration.rst +0 -0
  52. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/development.rst +0 -0
  53. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/index.rst +0 -0
  54. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/installation.rst +0 -0
  55. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/learning-path.rst +0 -0
  56. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/logging.rst +0 -0
  57. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/performance.md +0 -0
  58. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/progress-indicators.md +0 -0
  59. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/quickstart.rst +0 -0
  60. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/requirements.txt +0 -0
  61. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/security.rst +0 -0
  62. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/troubleshooting.rst +0 -0
  63. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/workflows.rst +0 -0
  64. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/docs/youtrack-concepts.rst +0 -0
  65. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/package-lock.json +0 -0
  66. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/package.json +0 -0
  67. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/__init__.py +0 -0
  68. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/conftest.py +0 -0
  69. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_auth.py +0 -0
  70. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_config.py +0 -0
  71. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_main.py +0 -0
  72. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tests/test_security.py +0 -0
  73. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/tox.ini +0 -0
  74. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/auth.py +0 -0
  75. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/cache.py +0 -0
  76. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/cli_utils/__init__.py +0 -0
  77. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/cli_utils/aliases.py +0 -0
  78. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/commands/__init__.py +0 -0
  79. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/commands/articles.py +0 -0
  80. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/commands/boards.py +0 -0
  81. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/commands/common.py +0 -0
  82. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/commands/issues.py +0 -0
  83. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/commands/projects.py +0 -0
  84. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/commands/time_tracking.py +0 -0
  85. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/commands/users.py +0 -0
  86. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/common.py +0 -0
  87. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/config.py +0 -0
  88. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/exceptions.py +0 -0
  89. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/logging.py +0 -0
  90. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/performance.py +0 -0
  91. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/progress.py +0 -0
  92. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/py.typed +0 -0
  93. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/security.py +0 -0
  94. {youtrack_cli-0.3.7 → youtrack_cli-0.3.9}/youtrack_cli/utils.py +0 -0
@@ -16,7 +16,7 @@ We use `uv` for managing dependencies.
16
16
 
17
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
18
 
19
- For every change that is implemented, the README.md file MUST be updated to reflect that change.
19
+ For every change that is implemented, the corresponding documentation in the docs/ flder MUST be updated to reflect that change. Updates to README.md should be made to include a very short summary but not comprehensive details.
20
20
 
21
21
  ## Test
22
22
 
@@ -30,6 +30,16 @@ Documentation is available in the docs/ folder. Any new functionality should hav
30
30
 
31
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
32
 
33
+ ## GitHub Issue resolution steps
34
+
35
+ 1. Make sure a new branch has been created
36
+ 2. Think through the change that needs to be implemented
37
+ 3. Write the plan to scratch/issue-id.md where id is the issue number from GitHub. For example issue 42 would be written to scratch/issue-42.md
38
+ 4. Implement the changes from the plan written in scratch/issue-id.md
39
+ 5. Create a PR. never bypass the pre-commit checks
40
+ 6. Once the PR has been squashed and merged, switch back to main. You'll need to check the PR status every 60 seconds
41
+ 7. Pull the changes from main to local development
42
+
33
43
  ## Current Configuration
34
44
 
35
45
  - 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.7
3
+ Version: 0.3.9
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/
@@ -53,7 +53,7 @@ format-check:
53
53
  typecheck:
54
54
  #!/usr/bin/env bash
55
55
  echo "Running ty type checker..."
56
- uv run ty youtrack_cli
56
+ uv run ty check --project 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.7"
3
+ version = "0.3.9"
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"
@@ -1,6 +1,6 @@
1
1
  """Tests for the admin module."""
2
2
 
3
- from unittest.mock import Mock, patch
3
+ from unittest.mock import AsyncMock, Mock, patch
4
4
 
5
5
  import httpx
6
6
  import pytest
@@ -46,12 +46,13 @@ class TestAdminManager:
46
46
  },
47
47
  ]
48
48
 
49
- with patch("httpx.AsyncClient") as mock_client:
49
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
50
+ mock_client_manager = Mock()
50
51
  mock_response = Mock()
51
52
  mock_response.json.return_value = mock_settings
52
- mock_response.raise_for_status.return_value = None
53
53
 
54
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
54
+ mock_client_manager.make_request = AsyncMock(return_value=mock_response)
55
+ mock_get_client.return_value = mock_client_manager
55
56
 
56
57
  result = await admin_manager.get_global_settings()
57
58
 
@@ -71,12 +72,14 @@ class TestAdminManager:
71
72
  @pytest.mark.asyncio
72
73
  async def test_get_global_settings_insufficient_permissions(self, admin_manager, auth_manager):
73
74
  """Test global settings retrieval with insufficient permissions."""
74
- with patch("httpx.AsyncClient") as mock_client:
75
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
76
+ mock_client_manager = Mock()
75
77
  mock_response = Mock()
76
78
  mock_response.status_code = 403
77
79
  mock_request = Mock()
78
80
  http_error = httpx.HTTPStatusError("Forbidden", request=mock_request, response=mock_response)
79
- mock_client.return_value.__aenter__.return_value.get.side_effect = http_error
81
+ mock_client_manager.make_request = AsyncMock(side_effect=http_error)
82
+ mock_get_client.return_value = mock_client_manager
80
83
 
81
84
  result = await admin_manager.get_global_settings()
82
85
 
@@ -86,11 +89,12 @@ class TestAdminManager:
86
89
  @pytest.mark.asyncio
87
90
  async def test_set_global_setting_success(self, admin_manager, auth_manager):
88
91
  """Test successful global setting update."""
89
- with patch("httpx.AsyncClient") as mock_client:
92
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
93
+ mock_client_manager = Mock()
90
94
  mock_response = Mock()
91
- mock_response.raise_for_status.return_value = None
92
95
 
93
- mock_client.return_value.__aenter__.return_value.post.return_value = mock_response
96
+ mock_client_manager.make_request = AsyncMock(return_value=mock_response)
97
+ mock_get_client.return_value = mock_client_manager
94
98
 
95
99
  result = await admin_manager.set_global_setting("server.name", "New Name")
96
100
 
@@ -100,12 +104,14 @@ class TestAdminManager:
100
104
  @pytest.mark.asyncio
101
105
  async def test_set_global_setting_invalid_data(self, admin_manager, auth_manager):
102
106
  """Test global setting update with invalid data."""
103
- with patch("httpx.AsyncClient") as mock_client:
107
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
108
+ mock_client_manager = Mock()
104
109
  mock_response = Mock()
105
110
  mock_response.status_code = 400
106
111
  mock_request = Mock()
107
112
  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
113
+ mock_client_manager.make_request = AsyncMock(side_effect=http_error)
114
+ mock_get_client.return_value = mock_client_manager
109
115
 
110
116
  result = await admin_manager.set_global_setting("invalid.key", "value")
111
117
 
@@ -123,12 +129,13 @@ class TestAdminManager:
123
129
  "isActive": True,
124
130
  }
125
131
 
126
- with patch("httpx.AsyncClient") as mock_client:
132
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
133
+ mock_client_manager = Mock()
127
134
  mock_response = Mock()
128
135
  mock_response.json.return_value = mock_license
129
- mock_response.raise_for_status.return_value = None
130
136
 
131
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
137
+ mock_client_manager.make_request = AsyncMock(return_value=mock_response)
138
+ mock_get_client.return_value = mock_client_manager
132
139
 
133
140
  result = await admin_manager.get_license_info()
134
141
 
@@ -140,12 +147,13 @@ class TestAdminManager:
140
147
  """Test successful license usage retrieval."""
141
148
  mock_usage = {"totalUsers": 75, "activeUsers": 50, "remainingUsers": 25}
142
149
 
143
- with patch("httpx.AsyncClient") as mock_client:
150
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
151
+ mock_client_manager = Mock()
144
152
  mock_response = Mock()
145
153
  mock_response.json.return_value = mock_usage
146
- mock_response.raise_for_status.return_value = None
147
154
 
148
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
155
+ mock_client_manager.make_request = AsyncMock(return_value=mock_response)
156
+ mock_get_client.return_value = mock_client_manager
149
157
 
150
158
  result = await admin_manager.get_license_usage()
151
159
 
@@ -163,12 +171,13 @@ class TestAdminManager:
163
171
  ],
164
172
  }
165
173
 
166
- with patch("httpx.AsyncClient") as mock_client:
174
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
175
+ mock_client_manager = Mock()
167
176
  mock_response = Mock()
168
177
  mock_response.json.return_value = mock_health
169
- mock_response.raise_for_status.return_value = None
170
178
 
171
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
179
+ mock_client_manager.make_request = AsyncMock(return_value=mock_response)
180
+ mock_get_client.return_value = mock_client_manager
172
181
 
173
182
  result = await admin_manager.get_system_health()
174
183
 
@@ -178,12 +187,14 @@ class TestAdminManager:
178
187
  @pytest.mark.asyncio
179
188
  async def test_get_system_health_404_error(self, admin_manager, auth_manager):
180
189
  """Test system health check with 404 error on all endpoints."""
181
- with patch("httpx.AsyncClient") as mock_client:
190
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
191
+ mock_client_manager = Mock()
182
192
  mock_response = Mock()
183
193
  mock_response.status_code = 404
184
194
  mock_request = Mock()
185
195
  http_error = httpx.HTTPStatusError("Not Found", request=mock_request, response=mock_response)
186
- mock_client.return_value.__aenter__.return_value.get.side_effect = http_error
196
+ mock_client_manager.make_request = AsyncMock(side_effect=http_error)
197
+ mock_get_client.return_value = mock_client_manager
187
198
 
188
199
  result = await admin_manager.get_system_health()
189
200
 
@@ -194,12 +205,14 @@ class TestAdminManager:
194
205
  @pytest.mark.asyncio
195
206
  async def test_get_system_health_403_error(self, admin_manager, auth_manager):
196
207
  """Test system health check with 403 permission error."""
197
- with patch("httpx.AsyncClient") as mock_client:
208
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
209
+ mock_client_manager = Mock()
198
210
  mock_response = Mock()
199
211
  mock_response.status_code = 403
200
212
  mock_request = Mock()
201
213
  http_error = httpx.HTTPStatusError("Forbidden", request=mock_request, response=mock_response)
202
- mock_client.return_value.__aenter__.return_value.get.side_effect = http_error
214
+ mock_client_manager.make_request = AsyncMock(side_effect=http_error)
215
+ mock_get_client.return_value = mock_client_manager
203
216
 
204
217
  result = await admin_manager.get_system_health()
205
218
 
@@ -210,11 +223,12 @@ class TestAdminManager:
210
223
  @pytest.mark.asyncio
211
224
  async def test_clear_caches_success(self, admin_manager, auth_manager):
212
225
  """Test successful cache clearing."""
213
- with patch("httpx.AsyncClient") as mock_client:
226
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
227
+ mock_client_manager = Mock()
214
228
  mock_response = Mock()
215
- mock_response.raise_for_status.return_value = None
216
229
 
217
- mock_client.return_value.__aenter__.return_value.post.return_value = mock_response
230
+ mock_client_manager.make_request = AsyncMock(return_value=mock_response)
231
+ mock_get_client.return_value = mock_client_manager
218
232
 
219
233
  result = await admin_manager.clear_caches()
220
234
 
@@ -239,12 +253,13 @@ class TestAdminManager:
239
253
  },
240
254
  ]
241
255
 
242
- with patch("httpx.AsyncClient") as mock_client:
256
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
257
+ mock_client_manager = Mock()
243
258
  mock_response = Mock()
244
259
  mock_response.json.return_value = {"usergroups": mock_groups}
245
- mock_response.raise_for_status.return_value = None
246
260
 
247
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
261
+ mock_client_manager.make_request = AsyncMock(return_value=mock_response)
262
+ mock_get_client.return_value = mock_client_manager
248
263
 
249
264
  result = await admin_manager.list_user_groups()
250
265
 
@@ -260,12 +275,13 @@ class TestAdminManager:
260
275
  "description": "A new group",
261
276
  }
262
277
 
263
- with patch("httpx.AsyncClient") as mock_client:
278
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
279
+ mock_client_manager = Mock()
264
280
  mock_response = Mock()
265
281
  mock_response.json.return_value = mock_created_group
266
- mock_response.raise_for_status.return_value = None
267
282
 
268
- mock_client.return_value.__aenter__.return_value.post.return_value = mock_response
283
+ mock_client_manager.make_request = AsyncMock(return_value=mock_response)
284
+ mock_get_client.return_value = mock_client_manager
269
285
 
270
286
  result = await admin_manager.create_user_group("New Group", "A new group")
271
287
 
@@ -276,12 +292,14 @@ class TestAdminManager:
276
292
  @pytest.mark.asyncio
277
293
  async def test_create_user_group_already_exists(self, admin_manager, auth_manager):
278
294
  """Test user group creation when group already exists."""
279
- with patch("httpx.AsyncClient") as mock_client:
295
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
296
+ mock_client_manager = Mock()
280
297
  mock_response = Mock()
281
298
  mock_response.status_code = 400
282
299
  mock_request = Mock()
283
300
  http_error = httpx.HTTPStatusError("Bad Request", request=mock_request, response=mock_response)
284
- mock_client.return_value.__aenter__.return_value.post.side_effect = http_error
301
+ mock_client_manager.make_request = AsyncMock(side_effect=http_error)
302
+ mock_get_client.return_value = mock_client_manager
285
303
 
286
304
  result = await admin_manager.create_user_group("Existing Group")
287
305
 
@@ -308,12 +326,13 @@ class TestAdminManager:
308
326
  },
309
327
  ]
310
328
 
311
- with patch("httpx.AsyncClient") as mock_client:
329
+ with patch("youtrack_cli.admin.get_client_manager") as mock_get_client:
330
+ mock_client_manager = Mock()
312
331
  mock_response = Mock()
313
332
  mock_response.json.return_value = mock_fields
314
- mock_response.raise_for_status.return_value = None
315
333
 
316
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_response
334
+ mock_client_manager.make_request = AsyncMock(return_value=mock_response)
335
+ mock_get_client.return_value = mock_client_manager
317
336
 
318
337
  result = await admin_manager.list_custom_fields()
319
338
 
@@ -1,7 +1,7 @@
1
1
  """Tests for article management functionality."""
2
2
 
3
3
  from pathlib import Path
4
- from unittest.mock import MagicMock, Mock, patch
4
+ from unittest.mock import AsyncMock, MagicMock, Mock, patch
5
5
 
6
6
  import pytest
7
7
  from click.testing import CliRunner
@@ -46,14 +46,15 @@ class TestArticleManager:
46
46
  "content": "Test content",
47
47
  }
48
48
 
49
- with patch("httpx.AsyncClient") as mock_client:
49
+ with patch("youtrack_cli.articles.get_client_manager") as mock_get_client:
50
+ mock_client_manager = Mock()
50
51
  mock_resp = Mock()
51
52
  mock_resp.status_code = 200
52
53
  mock_resp.json.return_value = mock_response
53
54
  mock_resp.text = '{"id": "123", "summary": "Test Article"}'
54
55
  mock_resp.headers = {"content-type": "application/json"}
55
- mock_resp.raise_for_status.return_value = None
56
- mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
56
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
57
+ mock_get_client.return_value = mock_client_manager
57
58
 
58
59
  result = await article_manager.create_article(
59
60
  title="Test Article",
@@ -67,11 +68,13 @@ class TestArticleManager:
67
68
  @pytest.mark.asyncio
68
69
  async def test_create_article_failure(self, article_manager):
69
70
  """Test article creation failure."""
70
- with patch("httpx.AsyncClient") as mock_client:
71
+ with patch("youtrack_cli.articles.get_client_manager") as mock_get_client:
72
+ mock_client_manager = Mock()
71
73
  mock_resp = Mock()
72
74
  mock_resp.status_code = 400
73
75
  mock_resp.text = "Bad Request"
74
- mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
76
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
77
+ mock_get_client.return_value = mock_client_manager
75
78
 
76
79
  result = await article_manager.create_article(
77
80
  title="Test Article",
@@ -97,14 +100,15 @@ class TestArticleManager:
97
100
  },
98
101
  ]
99
102
 
100
- with patch("httpx.AsyncClient") as mock_client:
103
+ with patch("youtrack_cli.articles.get_client_manager") as mock_get_client:
104
+ mock_client_manager = Mock()
101
105
  mock_resp = Mock()
102
106
  mock_resp.status_code = 200
103
107
  mock_resp.json.return_value = mock_response
104
108
  mock_resp.text = '[{"id": "123", "summary": "Article 1"}]'
105
109
  mock_resp.headers = {"content-type": "application/json"}
106
- mock_resp.raise_for_status.return_value = None
107
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
110
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
111
+ mock_get_client.return_value = mock_client_manager
108
112
 
109
113
  result = await article_manager.list_articles()
110
114
 
@@ -121,14 +125,15 @@ class TestArticleManager:
121
125
  "content": "Test content",
122
126
  }
123
127
 
124
- with patch("httpx.AsyncClient") as mock_client:
128
+ with patch("youtrack_cli.articles.get_client_manager") as mock_get_client:
129
+ mock_client_manager = Mock()
125
130
  mock_resp = Mock()
126
131
  mock_resp.status_code = 200
127
132
  mock_resp.json.return_value = mock_response
128
133
  mock_resp.text = '{"mock": "response"}'
129
134
  mock_resp.headers = {"content-type": "application/json"}
130
- mock_resp.raise_for_status.return_value = None
131
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
135
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
136
+ mock_get_client.return_value = mock_client_manager
132
137
 
133
138
  result = await article_manager.get_article("123")
134
139
 
@@ -144,14 +149,15 @@ class TestArticleManager:
144
149
  "content": "Updated content",
145
150
  }
146
151
 
147
- with patch("httpx.AsyncClient") as mock_client:
152
+ with patch("youtrack_cli.articles.get_client_manager") as mock_get_client:
153
+ mock_client_manager = Mock()
148
154
  mock_resp = Mock()
149
155
  mock_resp.status_code = 200
150
156
  mock_resp.json.return_value = mock_response
151
157
  mock_resp.text = '{"mock": "response"}'
152
158
  mock_resp.headers = {"content-type": "application/json"}
153
- mock_resp.raise_for_status.return_value = None
154
- mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
159
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
160
+ mock_get_client.return_value = mock_client_manager
155
161
 
156
162
  result = await article_manager.update_article(
157
163
  article_id="123",
@@ -174,11 +180,12 @@ class TestArticleManager:
174
180
  @pytest.mark.asyncio
175
181
  async def test_delete_article_success(self, article_manager):
176
182
  """Test successful article deletion."""
177
- with patch("httpx.AsyncClient") as mock_client:
183
+ with patch("youtrack_cli.articles.get_client_manager") as mock_get_client:
184
+ mock_client_manager = Mock()
178
185
  mock_resp = Mock()
179
186
  mock_resp.status_code = 200
180
- mock_resp.raise_for_status.return_value = None
181
- mock_client.return_value.__aenter__.return_value.delete.return_value = mock_resp # noqa: E501
187
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
188
+ mock_get_client.return_value = mock_client_manager
182
189
 
183
190
  result = await article_manager.delete_article("123")
184
191
 
@@ -194,14 +201,15 @@ class TestArticleManager:
194
201
  "visibility": {"type": "public"},
195
202
  }
196
203
 
197
- with patch("httpx.AsyncClient") as mock_client:
204
+ with patch("youtrack_cli.articles.get_client_manager") as mock_get_client:
205
+ mock_client_manager = Mock()
198
206
  mock_resp = Mock()
199
207
  mock_resp.status_code = 200
200
208
  mock_resp.json.return_value = mock_response
201
209
  mock_resp.text = '{"mock": "response"}'
202
210
  mock_resp.headers = {"content-type": "application/json"}
203
- mock_resp.raise_for_status.return_value = None
204
- mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
211
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
212
+ mock_get_client.return_value = mock_client_manager
205
213
 
206
214
  result = await article_manager.publish_article("123")
207
215
 
@@ -220,14 +228,15 @@ class TestArticleManager:
220
228
  }
221
229
  ]
222
230
 
223
- with patch("httpx.AsyncClient") as mock_client:
231
+ with patch("youtrack_cli.articles.get_client_manager") as mock_get_client:
232
+ mock_client_manager = Mock()
224
233
  mock_resp = Mock()
225
234
  mock_resp.status_code = 200
226
235
  mock_resp.json.return_value = mock_response
227
236
  mock_resp.text = '{"mock": "response"}'
228
237
  mock_resp.headers = {"content-type": "application/json"}
229
- mock_resp.raise_for_status.return_value = None
230
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
238
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
239
+ mock_get_client.return_value = mock_client_manager
231
240
 
232
241
  result = await article_manager.search_articles("search query")
233
242
 
@@ -246,14 +255,15 @@ class TestArticleManager:
246
255
  }
247
256
  ]
248
257
 
249
- with patch("httpx.AsyncClient") as mock_client:
258
+ with patch("youtrack_cli.articles.get_client_manager") as mock_get_client:
259
+ mock_client_manager = Mock()
250
260
  mock_resp = Mock()
251
261
  mock_resp.status_code = 200
252
262
  mock_resp.json.return_value = mock_response
253
263
  mock_resp.text = '{"mock": "response"}'
254
264
  mock_resp.headers = {"content-type": "application/json"}
255
- mock_resp.raise_for_status.return_value = None
256
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
265
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
266
+ mock_get_client.return_value = mock_client_manager
257
267
 
258
268
  result = await article_manager.get_article_comments("123")
259
269
 
@@ -269,14 +279,15 @@ class TestArticleManager:
269
279
  "author": {"fullName": "Test User"},
270
280
  }
271
281
 
272
- with patch("httpx.AsyncClient") as mock_client:
282
+ with patch("youtrack_cli.articles.get_client_manager") as mock_get_client:
283
+ mock_client_manager = Mock()
273
284
  mock_resp = Mock()
274
285
  mock_resp.status_code = 200
275
286
  mock_resp.json.return_value = mock_response
276
287
  mock_resp.text = '{"mock": "response"}'
277
288
  mock_resp.headers = {"content-type": "application/json"}
278
- mock_resp.raise_for_status.return_value = None
279
- mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp # noqa: E501
289
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
290
+ mock_get_client.return_value = mock_client_manager
280
291
 
281
292
  result = await article_manager.add_comment("123", "Test comment")
282
293
 
@@ -296,14 +307,15 @@ class TestArticleManager:
296
307
  }
297
308
  ]
298
309
 
299
- with patch("httpx.AsyncClient") as mock_client:
310
+ with patch("youtrack_cli.articles.get_client_manager") as mock_get_client:
311
+ mock_client_manager = Mock()
300
312
  mock_resp = Mock()
301
313
  mock_resp.status_code = 200
302
314
  mock_resp.json.return_value = mock_response
303
315
  mock_resp.text = '{"mock": "response"}'
304
316
  mock_resp.headers = {"content-type": "application/json"}
305
- mock_resp.raise_for_status.return_value = None
306
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp # noqa: E501
317
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
318
+ mock_get_client.return_value = mock_client_manager
307
319
 
308
320
  result = await article_manager.get_article_attachments("123")
309
321
 
@@ -1,6 +1,6 @@
1
1
  """Tests for board management functionality."""
2
2
 
3
- from unittest.mock import MagicMock, Mock, patch
3
+ from unittest.mock import AsyncMock, MagicMock, Mock, patch
4
4
 
5
5
  import pytest
6
6
 
@@ -47,15 +47,16 @@ class TestBoardManager:
47
47
  }
48
48
  ]
49
49
 
50
- with patch("httpx.AsyncClient") as mock_client:
50
+ with patch("youtrack_cli.boards.get_client_manager") as mock_get_client_manager:
51
51
  mock_resp = Mock()
52
52
  mock_resp.status_code = 200
53
53
  mock_resp.json.return_value = mock_response
54
54
  mock_resp.text = '{"mock": "response"}'
55
55
  mock_resp.headers = {"content-type": "application/json"}
56
- mock_resp.raise_for_status.return_value = None
57
56
 
58
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp
57
+ mock_client_manager = Mock()
58
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
59
+ mock_get_client_manager.return_value = mock_client_manager
59
60
 
60
61
  result = await board_manager.list_boards()
61
62
 
@@ -74,15 +75,16 @@ class TestBoardManager:
74
75
  }
75
76
  ]
76
77
 
77
- with patch("httpx.AsyncClient") as mock_client:
78
+ with patch("youtrack_cli.boards.get_client_manager") as mock_get_client_manager:
78
79
  mock_resp = Mock()
79
80
  mock_resp.status_code = 200
80
81
  mock_resp.json.return_value = mock_response
81
82
  mock_resp.text = '{"mock": "response"}'
82
83
  mock_resp.headers = {"content-type": "application/json"}
83
- mock_resp.raise_for_status.return_value = None
84
84
 
85
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp
85
+ mock_client_manager = Mock()
86
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
87
+ mock_get_client_manager.return_value = mock_client_manager
86
88
 
87
89
  result = await board_manager.list_boards(project_id="TEST")
88
90
 
@@ -112,15 +114,16 @@ class TestBoardManager:
112
114
  "sprints": [{"name": "Sprint 1"}],
113
115
  }
114
116
 
115
- with patch("httpx.AsyncClient") as mock_client:
117
+ with patch("youtrack_cli.boards.get_client_manager") as mock_get_client_manager:
116
118
  mock_resp = Mock()
117
119
  mock_resp.status_code = 200
118
120
  mock_resp.json.return_value = mock_response
119
121
  mock_resp.text = '{"mock": "response"}'
120
122
  mock_resp.headers = {"content-type": "application/json"}
121
- mock_resp.raise_for_status.return_value = None
122
123
 
123
- mock_client.return_value.__aenter__.return_value.get.return_value = mock_resp
124
+ mock_client_manager = Mock()
125
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
126
+ mock_get_client_manager.return_value = mock_client_manager
124
127
 
125
128
  result = await board_manager.view_board("123")
126
129
 
@@ -147,15 +150,16 @@ class TestBoardManager:
147
150
  "owner": {"name": "Test User"},
148
151
  }
149
152
 
150
- with patch("httpx.AsyncClient") as mock_client:
153
+ with patch("youtrack_cli.boards.get_client_manager") as mock_get_client_manager:
151
154
  mock_resp = Mock()
152
155
  mock_resp.status_code = 200
153
156
  mock_resp.json.return_value = mock_response
154
157
  mock_resp.text = '{"mock": "response"}'
155
158
  mock_resp.headers = {"content-type": "application/json"}
156
- mock_resp.raise_for_status.return_value = None
157
159
 
158
- mock_client.return_value.__aenter__.return_value.post.return_value = mock_resp
160
+ mock_client_manager = Mock()
161
+ mock_client_manager.make_request = AsyncMock(return_value=mock_resp)
162
+ mock_get_client_manager.return_value = mock_client_manager
159
163
 
160
164
  result = await board_manager.update_board("123", name="Updated Board Name")
161
165
 
@@ -184,8 +188,10 @@ class TestBoardManager:
184
188
  @pytest.mark.asyncio
185
189
  async def test_list_boards_general_error(self, board_manager):
186
190
  """Test board listing with general error."""
187
- with patch("httpx.AsyncClient") as mock_client:
188
- mock_client.return_value.__aenter__.return_value.get.side_effect = Exception("Connection error")
191
+ with patch("youtrack_cli.boards.get_client_manager") as mock_get_client_manager:
192
+ mock_client_manager = Mock()
193
+ mock_client_manager.make_request = AsyncMock(side_effect=Exception("Connection error"))
194
+ mock_get_client_manager.return_value = mock_client_manager
189
195
 
190
196
  result = await board_manager.list_boards()
191
197
 
@@ -195,8 +201,10 @@ class TestBoardManager:
195
201
  @pytest.mark.asyncio
196
202
  async def test_view_board_general_error(self, board_manager):
197
203
  """Test board viewing with general error."""
198
- with patch("httpx.AsyncClient") as mock_client:
199
- mock_client.return_value.__aenter__.return_value.get.side_effect = Exception("Connection error")
204
+ with patch("youtrack_cli.boards.get_client_manager") as mock_get_client_manager:
205
+ mock_client_manager = Mock()
206
+ mock_client_manager.make_request = AsyncMock(side_effect=Exception("Connection error"))
207
+ mock_get_client_manager.return_value = mock_client_manager
200
208
 
201
209
  result = await board_manager.view_board("123")
202
210
 
@@ -206,8 +214,10 @@ class TestBoardManager:
206
214
  @pytest.mark.asyncio
207
215
  async def test_update_board_general_error(self, board_manager):
208
216
  """Test board updating with general error."""
209
- with patch("httpx.AsyncClient") as mock_client:
210
- mock_client.return_value.__aenter__.return_value.post.side_effect = Exception("Connection error")
217
+ with patch("youtrack_cli.boards.get_client_manager") as mock_get_client_manager:
218
+ mock_client_manager = Mock()
219
+ mock_client_manager.make_request = AsyncMock(side_effect=Exception("Connection error"))
220
+ mock_get_client_manager.return_value = mock_client_manager
211
221
 
212
222
  result = await board_manager.update_board("123", name="New Name")
213
223