forked from pyvec/python.cz
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub_test.py
More file actions
387 lines (306 loc) · 12.8 KB
/
Copy pathgithub_test.py
File metadata and controls
387 lines (306 loc) · 12.8 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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
import re
import warnings
import pytest
import requests
import responses
from werkzeug.contrib.cache import NullCache
from pythoncz.models import github as github_module
from pythoncz_tests.models import github_test_fixtures as fixtures
class RequestsMock(responses.RequestsMock):
def add_github_graphql(self, *args, **kwargs):
"""
Convenience method to simplify adding fake responses for calls
to GitHub's v4 GraphQL API
"""
return self.add(responses.POST, 'https://api.github.com/graphql',
*args, **kwargs)
@pytest.fixture()
def requests_mock():
"""Provides mechanism for faking HTTP responses"""
with RequestsMock() as mock:
yield mock
@pytest.fixture()
def github():
"""Provides the 'github' module with caching disabled"""
original_cache = github_module.cache
github_module.cache = NullCache()
yield github_module
github_module.cache = original_cache
def test_get_issues_merges_orgs(github, requests_mock):
"""
Tests whether 'get_issues' makes two HTTP requests for each given GitHub
organization name and whether both data gets combined in the results
"""
issue_org1 = fixtures.issue()
requests_mock.add_github_graphql(
json=fixtures.api_response_body(repos=[
fixtures.repository(issues=[issue_org1], pull_requests=[],
private=False),
]),
)
issue_org2 = fixtures.issue()
requests_mock.add_github_graphql(
json=fixtures.api_response_body(repos=[
fixtures.repository(issues=[issue_org2], pull_requests=[],
private=False),
]),
)
issues = github.get_issues(['org1', 'org2'])
titles = set(i['title'] for i in issues)
assert titles == {issue_org1['title'], issue_org2['title']}
def test_get_issues_api_error(github, requests_mock):
"""
In case of API error, the 'get_issues' function should raise HTTPError
with combined error messages
"""
requests_mock.add_github_graphql(
status=500,
json={'errors': [{'message': 'Error 1'}, {'message': 'Error 2'}]},
)
error_message = 'Error 1; Error 2'
with pytest.raises(requests.HTTPError, match=error_message) as excinfo:
github.get_issues(['org'])
assert excinfo.value.response.status_code == 500
def test_create_api_session(github):
"""
The '_create_api_session' helper should add User-Agent and Authorization
HTTP headers to the HTTP session
"""
session = github._create_api_session()
headers = session.headers
assert 'https://python.cz' in headers.get('User-Agent')
assert re.match(r'token \w+', headers.get('Authorization'))
def test_get_issues_for_org_key_error(github, requests_mock):
"""
If the API response doesn't have the expected structure, the
'_get_issues_for_org' helper should raise ValueError
"""
requests_mock.add_github_graphql(json={'data': {}})
error_message = 'Unexpected structure of the GitHub API response'
with pytest.raises(ValueError, message=error_message):
list(github._get_issues_for_org(requests.Session(), 'org'))
def test_get_issues_for_org_merges_issues_pull_requests(github, requests_mock):
"""
The '_get_issues_for_org' helper should merge both issues with
pull requests and treat them both as issues
"""
repo1 = fixtures.repository(
issues=[fixtures.issue(), fixtures.issue(), fixtures.issue()],
pull_requests=[fixtures.issue(pr=True)],
private=False
)
repo2 = fixtures.repository(
issues=[fixtures.issue(), fixtures.issue()],
pull_requests=[fixtures.issue(pr=True), fixtures.issue(pr=True)],
private=False
)
expected_titles_are_pr = {}
for repo in [repo1, repo2]:
for issue in repo['issues']['nodes']:
expected_titles_are_pr[issue['title']] = False
for pull_request in repo['pullRequests']['nodes']:
expected_titles_are_pr[pull_request['title']] = True
api_response_body = fixtures.api_response_body(repos=[repo1, repo2])
requests_mock.add_github_graphql(json=api_response_body)
issues = github._get_issues_for_org(requests.Session(), 'org')
titles_are_pr = {i['title']: i['is_pull_request'] for i in issues}
assert titles_are_pr == expected_titles_are_pr
def test_get_issues_for_org_skips_private(github, requests_mock):
"""
The '_get_issues_for_org' helper should skip private repositories
"""
public_repo = fixtures.repository(private=False)
expected_titles = set()
for issue in public_repo['issues']['nodes']:
expected_titles.add(issue['title'])
for pull_request in public_repo['pullRequests']['nodes']:
expected_titles.add(pull_request['title'])
api_response_body = fixtures.api_response_body(repos=[
fixtures.repository(private=True),
public_repo,
fixtures.repository(private=True),
fixtures.repository(private=True),
fixtures.repository(private=True),
])
requests_mock.add_github_graphql(json=api_response_body)
issues = github._get_issues_for_org(requests.Session(), 'org')
titles = set(i['title'] for i in issues)
assert titles == expected_titles
def test_request_api_200_invalid_json(github, requests_mock):
"""
The '_request_api' helper should raise 'ValueError' if the JSON in the
response cannot be decoded
"""
requests_mock.add_github_graphql(body='... invalid JSON ...')
error_message = 'Unexpected structure of the GitHub API response'
with pytest.raises(ValueError, message=error_message):
github._request_api(requests.Session(), '... query ...', {})
def test_request_api_500_invalid_json(github, requests_mock):
"""
The '_request_api' helper should raise HTTP error even if the JSON in the
response cannot be decoded
"""
requests_mock.add_github_graphql(status=500, body='... invalid JSON ...')
with pytest.raises(requests.HTTPError) as excinfo:
github._request_api(requests.Session(), '... query ...', {})
assert excinfo.value.response.status_code == 500
@pytest.mark.parametrize('status_code', (200, 500))
def test_request_api_X00_errors(github, requests_mock, status_code):
"""
The '_request_api' helper should raise HTTP error with error messages
sent in the response body if they're present, regardless of the HTTP status
code
"""
requests_mock.add_github_graphql(
status=status_code,
json={'errors': [{'message': 'Error 1'}, {'message': 'Error 2'}]},
)
error_message = 'Error 1; Error 2'
with pytest.raises(requests.HTTPError, match=error_message) as excinfo:
github._request_api(requests.Session(), '... query ...', {})
assert excinfo.value.response.status_code == status_code
def test_request_api_200(github, requests_mock):
"""
The '_request_api' helper should parse valid JSON response and return it
"""
requests_mock.add_github_graphql(json={'data': '...'})
json = github._request_api(requests.Session(), '... query ...', {})
assert json == {'data': '...'}
def test_request_api_500(github, requests_mock):
"""
The '_request_api' helper should raise HTTP error if the HTTP status code
indicates HTTP error, even if there are no errors in the JSON response
"""
requests_mock.add_github_graphql(status=500, json={'data': '...'})
with pytest.raises(requests.HTTPError) as excinfo:
github._request_api(requests.Session(), '... query ...', {})
assert excinfo.value.response.status_code == 500
def test_format_issue_missing_author(github):
"""
The '_format_issue' helper should be able to deal with the situation
when the author is deleted/disabled user
"""
issue = fixtures.issue()
issue['author'] = None
formatted_issue = github._format_issue('org', fixtures.repository(), issue)
assert formatted_issue['user'] == {
'login': None,
'html_url': 'https://github.com/ghost',
}
def test_format_issue_labels(github):
"""The '_format_issue' helper should be able to process labels"""
issue = fixtures.issue(labels=[{'name': 'bug'}, {'name': 'feature'}])
formatted_issue = github._format_issue('org', fixtures.repository(), issue)
assert formatted_issue['labels'] == ['bug', 'feature']
assert formatted_issue['coach'] is False
@pytest.mark.parametrize('label_name', ('coach', 'sprint-idea'))
def test_format_issue_special_labels(github, label_name):
"""
The '_format_issue' helper should be able to process the 'coach' label
and to mark the resulting formatted issue with the 'coach' flag accordingly
"""
issue = fixtures.issue(labels=[{'name': 'bug'}, {'name': label_name}])
formatted_issue = github._format_issue('org', fixtures.repository(), issue)
assert formatted_issue['labels'] == ['bug', label_name]
assert formatted_issue[label_name] is True
def test_format_issue_reactions(github):
"""
The '_format_issue' helper should be able to calculate 'votes'
from reactions
"""
issue = fixtures.issue(reactions_counts={
'LAUGH': 42,
'HEART': 3,
})
formatted_issue = github._format_issue('org', fixtures.repository(), issue)
assert formatted_issue['votes'] == 42 + 3
def test_calculate_votes(github):
"""
The '_calculate_votes' helper should correctly deal with negative votes
"""
votes = github._calculate_votes(fixtures.issue(reactions_counts={
'THUMBS_UP': 1,
'THUMBS_DOWN': 1,
'LAUGH': 1,
'HOORAY': 1,
'CONFUSED': 1,
'HEART': 1,
}))
assert votes == 2
def test_get_nodes_without_total_count(github):
subnodes = [1, 2, 3, 4]
result = github._get_nodes({'something': {'nodes': subnodes}}, 'something')
assert result == subnodes
def test_get_nodes_with_total_count(github):
subnodes = [1, 2, 3, 4]
result = github._get_nodes({'something': {
'totalCount': len(subnodes),
'nodes': subnodes,
}}, 'something')
assert result == subnodes
def test_get_nodes_with_different_total_count(github):
"""
If the total count of sub-nodes is different then the number of sub-nodes
available in the API response, at least warn about the fact that there's
some data missing and limits should be raised or results paginated
"""
subnodes = [1, 2, 3, 4]
node = {'something': {
'totalCount': len(subnodes) + 42,
'nodes': subnodes,
}}
with warnings.catch_warnings(record=True) as recorded_warnings:
result = github._get_nodes(node, 'something')
assert result == subnodes
assert len(recorded_warnings) == 1
assert issubclass(recorded_warnings[0].category, UserWarning)
assert 'nodes in total, but only' in str(recorded_warnings[0].message)
def test_sort_issues_coach(github):
"""
Issues with coaching offer should always go first no matter what
"""
repository = fixtures.repository()
issues = [github._format_issue('org', repository, issue) for issue in [
fixtures.issue(labels=[]),
fixtures.issue(labels=[{'name': 'foo'}]),
fixtures.issue(labels=[{'name': 'bar'}, {'name': 'coach'}]),
fixtures.issue(labels=[]),
]]
sorted_issues = github._sort_issues(issues)
assert sorted_issues[0]['coach'] is True
def test_sort_issues_votes(github):
"""
Issues with most votes should go first if there's no 'coach' issue
"""
repository = fixtures.repository()
issues = [github._format_issue('org', repository, issue) for issue in [
fixtures.issue(labels=[], reactions_counts={'THUMBS_UP': 1}),
fixtures.issue(labels=[], reactions_counts={'THUMBS_DOWN': 1}),
fixtures.issue(labels=[], reactions_counts={'THUMBS_UP': 4}),
fixtures.issue(labels=[], reactions_counts={'THUMBS_UP': 3}),
]]
sorted_issues = github._sort_issues(issues)
assert sorted_issues[0]['votes'] == 4
def test_sort_issues_activity(github):
"""
Issues with most user activity should go first if there's no 'coach'
issue and no reactions.
User activity is comments count + participants count
"""
issue1 = fixtures.issue(labels=[], reactions_counts={})
issue1['comments']['totalCount'] = 3
issue1['participants']['totalCount'] = 2
issue2 = fixtures.issue(labels=[], reactions_counts={})
issue2['comments']['totalCount'] = 1
issue2['participants']['totalCount'] = 9
issue3 = fixtures.issue(labels=[], reactions_counts={})
issue3['comments']['totalCount'] = 0
issue3['participants']['totalCount'] = 4
repository = fixtures.repository()
sorted_issues = github._sort_issues([
github._format_issue('org', repository, issue)
for issue in [issue1, issue2, issue3]
])
assert sorted_issues[0]['comments'] == 1 # + 9 = 10
assert sorted_issues[1]['comments'] == 3 # + 2 = 7
assert sorted_issues[2]['comments'] == 0 # + 4 = 4