-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathtest_github_integration.py
More file actions
370 lines (301 loc) · 12.6 KB
/
Copy pathtest_github_integration.py
File metadata and controls
370 lines (301 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
"""Unit tests for GitHubIntegration (TDD - written before implementation)."""
import pytest
from datetime import datetime, UTC
from unittest.mock import AsyncMock, patch
from codeframe.git.github_integration import (
GitHubIntegration,
PRDetails,
MergeResult,
GitHubAPIError,
)
class TestPRDetails:
"""Tests for PRDetails data class."""
def test_pr_details_creation(self):
"""Test creating a PRDetails object."""
pr = PRDetails(
number=42,
url="https://github.com/owner/repo/pull/42",
state="open",
title="Test PR",
body="Test body",
created_at=datetime.now(UTC),
merged_at=None,
head_branch="feature/test",
base_branch="main",
)
assert pr.number == 42
assert pr.state == "open"
assert pr.title == "Test PR"
assert pr.merged_at is None
class TestMergeResult:
"""Tests for MergeResult data class."""
def test_merge_result_success(self):
"""Test creating a successful MergeResult."""
result = MergeResult(
sha="abc123def456",
merged=True,
message="Pull Request successfully merged",
)
assert result.merged is True
assert result.sha == "abc123def456"
def test_merge_result_failure(self):
"""Test creating a failed MergeResult."""
result = MergeResult(
sha=None,
merged=False,
message="Pull Request is not mergeable",
)
assert result.merged is False
assert result.sha is None
class TestGitHubIntegration:
"""Tests for GitHubIntegration class."""
@pytest.fixture
def github(self):
"""Create GitHubIntegration instance."""
return GitHubIntegration(
token="ghp_test_token_12345",
repo="owner/test-repo",
)
def test_init_parses_repo_correctly(self, github):
"""Test that repo is parsed correctly."""
assert github.owner == "owner"
assert github.repo_name == "test-repo"
def test_init_with_invalid_repo_format(self):
"""Test that invalid repo format raises error."""
with pytest.raises(ValueError, match="Invalid repo format"):
GitHubIntegration(token="token", repo="invalid-format")
@pytest.mark.asyncio
async def test_create_pull_request_success(self, github):
"""Test successful PR creation."""
mock_response = {
"number": 42,
"html_url": "https://github.com/owner/test-repo/pull/42",
"state": "open",
"title": "Test PR",
"body": "Test body",
"created_at": "2024-01-15T10:30:00Z",
"merged_at": None,
"head": {"ref": "feature/test"},
"base": {"ref": "main"},
}
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.return_value = mock_response
pr_details = await github.create_pull_request(
branch="feature/test",
title="Test PR",
body="Test body",
base="main",
)
assert pr_details.number == 42
assert pr_details.state == "open"
assert pr_details.title == "Test PR"
# Verify API was called correctly
mock_request.assert_called_once()
call_kwargs = mock_request.call_args.kwargs
assert call_kwargs["method"] == "POST"
assert "pulls" in call_kwargs["endpoint"]
@pytest.mark.asyncio
async def test_create_pull_request_api_error(self, github):
"""Test PR creation with API error."""
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.side_effect = GitHubAPIError(
status_code=422,
message="Validation Failed",
)
with pytest.raises(GitHubAPIError) as exc_info:
await github.create_pull_request(
branch="feature/test",
title="Test PR",
body="Test body",
)
assert exc_info.value.status_code == 422
@pytest.mark.asyncio
async def test_get_pull_request_success(self, github):
"""Test getting PR details."""
mock_response = {
"number": 42,
"html_url": "https://github.com/owner/test-repo/pull/42",
"state": "open",
"title": "Test PR",
"body": "Test body",
"created_at": "2024-01-15T10:30:00Z",
"merged_at": None,
"head": {"ref": "feature/test"},
"base": {"ref": "main"},
}
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.return_value = mock_response
pr_details = await github.get_pull_request(42)
assert pr_details.number == 42
mock_request.assert_called_once()
@pytest.mark.asyncio
async def test_get_pull_request_not_found(self, github):
"""Test getting non-existent PR."""
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.side_effect = GitHubAPIError(
status_code=404,
message="Not Found",
)
with pytest.raises(GitHubAPIError) as exc_info:
await github.get_pull_request(99999)
assert exc_info.value.status_code == 404
@pytest.mark.asyncio
async def test_list_pull_requests_success(self, github):
"""Test listing PRs."""
mock_response = [
{
"number": 1,
"html_url": "https://github.com/owner/test-repo/pull/1",
"state": "open",
"title": "PR 1",
"body": "Body 1",
"created_at": "2024-01-15T10:30:00Z",
"merged_at": None,
"head": {"ref": "feature/1"},
"base": {"ref": "main"},
},
{
"number": 2,
"html_url": "https://github.com/owner/test-repo/pull/2",
"state": "open",
"title": "PR 2",
"body": "Body 2",
"created_at": "2024-01-16T10:30:00Z",
"merged_at": None,
"head": {"ref": "feature/2"},
"base": {"ref": "main"},
},
]
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.return_value = mock_response
prs = await github.list_pull_requests(state="open")
assert len(prs) == 2
assert prs[0].number == 1
assert prs[1].number == 2
@pytest.mark.asyncio
async def test_merge_pull_request_success(self, github):
"""Test successful PR merge."""
mock_response = {
"sha": "abc123def456",
"merged": True,
"message": "Pull Request successfully merged",
}
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.return_value = mock_response
result = await github.merge_pull_request(42, method="squash")
assert result.merged is True
assert result.sha == "abc123def456"
# Verify merge method was passed
mock_request.assert_called_once()
call_kwargs = mock_request.call_args.kwargs
assert "merge" in call_kwargs["endpoint"]
@pytest.mark.asyncio
async def test_merge_pull_request_not_mergeable(self, github):
"""Test merge with non-mergeable PR."""
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.side_effect = GitHubAPIError(
status_code=405,
message="Pull Request is not mergeable",
)
with pytest.raises(GitHubAPIError) as exc_info:
await github.merge_pull_request(42)
assert exc_info.value.status_code == 405
@pytest.mark.asyncio
async def test_close_pull_request_success(self, github):
"""Test closing a PR."""
mock_response = {
"number": 42,
"html_url": "https://github.com/owner/test-repo/pull/42",
"state": "closed",
"title": "Test PR",
"body": "Test body",
"created_at": "2024-01-15T10:30:00Z",
"merged_at": None,
"head": {"ref": "feature/test"},
"base": {"ref": "main"},
}
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.return_value = mock_response
result = await github.close_pull_request(42)
assert result is True
mock_request.assert_called_once()
call_kwargs = mock_request.call_args.kwargs
assert call_kwargs["method"] == "PATCH"
@pytest.mark.asyncio
async def test_authentication_error(self, github):
"""Test handling of authentication errors."""
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.side_effect = GitHubAPIError(
status_code=401,
message="Bad credentials",
)
with pytest.raises(GitHubAPIError) as exc_info:
await github.get_pull_request(42)
assert exc_info.value.status_code == 401
@pytest.mark.asyncio
async def test_rate_limit_error(self, github):
"""Test handling of rate limit errors."""
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.side_effect = GitHubAPIError(
status_code=403,
message="API rate limit exceeded",
)
with pytest.raises(GitHubAPIError) as exc_info:
await github.get_pull_request(42)
assert exc_info.value.status_code == 403
@pytest.mark.v2
class TestGetPrFiles:
"""Tests for GitHubIntegration.get_pr_files."""
@pytest.fixture
def github(self):
return GitHubIntegration(
token="ghp_test_token_12345",
repo="owner/test-repo",
)
@pytest.mark.asyncio
async def test_returns_list_of_filenames(self, github):
"""get_pr_files returns a list of filename strings."""
mock_response = [
{"filename": "src/app.py", "status": "modified"},
{"filename": "tests/test_app.py", "status": "added"},
]
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.return_value = mock_response
files = await github.get_pr_files(42)
assert files == ["src/app.py", "tests/test_app.py"]
mock_request.assert_called_once()
call_kwargs = mock_request.call_args.kwargs
assert call_kwargs["method"] == "GET"
assert "/pulls/42/files" in call_kwargs["endpoint"]
assert "per_page=100" in call_kwargs["endpoint"]
@pytest.mark.asyncio
async def test_returns_empty_list_for_no_files(self, github):
"""get_pr_files returns empty list when PR has no file changes."""
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.return_value = []
files = await github.get_pr_files(1)
assert files == []
@pytest.mark.asyncio
async def test_propagates_api_error(self, github):
"""get_pr_files propagates GitHubAPIError."""
with patch.object(github, "_make_request", new_callable=AsyncMock) as mock_request:
mock_request.side_effect = GitHubAPIError(404, "Not Found")
with pytest.raises(GitHubAPIError) as exc_info:
await github.get_pr_files(99999)
assert exc_info.value.status_code == 404
class TestGitHubAPIError:
"""Tests for GitHubAPIError exception."""
def test_error_message(self):
"""Test error message formatting."""
error = GitHubAPIError(status_code=404, message="Not Found")
assert "404" in str(error)
assert "Not Found" in str(error)
def test_error_with_details(self):
"""Test error with additional details."""
error = GitHubAPIError(
status_code=422,
message="Validation Failed",
details={"errors": [{"field": "title", "code": "missing"}]},
)
assert error.details is not None
assert "title" in str(error.details)