Backporting production changes / updating tests
[margebot.git] / tests / test_marge.py
1 """
2 Margebot tests
3 """
4 # pragma pylint: disable=invalid-name
5 # pragma pylint: disable=missing-docstring
6 # pragma pylint: disable=no-else-return
7 # pragma pylint: disable=protected-access
8 # pragma pylint: disable=redefined-outer-name
9 # pragma pylint: disable=too-many-public-methods
10 # pragma pylint: disable=unused-argument
11
12 from datetime import datetime
13 import logging
14 import json
15 from queue import Empty
16
17 from dateutil.relativedelta import relativedelta
18 import requests
19 from errbot.backends.test import testbot # pylint: disable=unused-import
20 import errbot
21 import pytest
22 from httmock import urlmatch, HTTMock
23 from plugins.marge import deltastr
24
25
26 # HTTMock urlmatchers
27
28
29 @urlmatch(path=r'/api/v4/users/author_id$')
30 def mock_users_get_author_id(url, request):
31 return {'status_code': 200,
32 'headers': {'content-type': 'application/json'},
33 'content': json.dumps({'username': 'author_id username', 'id': 3001})}
34
35
36 @urlmatch(path=r'/api/v4/users/[^/\?]+$')
37 def mock_users_get_unexpected(url, request):
38 return {'status_code': 404}
39
40
41 @urlmatch(path=r'/api/v4/users', query='username=gbin')
42 def mock_users_search_gbin(url, request):
43 return {'status_code': 200,
44 'headers': {'content-type': 'application/json'},
45 'content': json.dumps([{'username': 'gbin', 'id': 3002}])}
46
47
48 @urlmatch(path=r'^/api/v4/projects/[^/]+/hooks$')
49 def mock_projects_hooks_list(url, request):
50 if request.method == "GET":
51 return {'status_code': 200,
52 'headers': {'content-type': 'application/json'},
53 'content': '[]'}
54 elif request.method == "POST":
55 return {'status_code': 200,
56 'headers': {'content-type': 'application/json'},
57 'content': json.dumps({"id": "hook_id",
58 "merge_requests_events": True,
59 "url": "url"})}
60 elif request.method == "PUT":
61 return {'status_code': 200,
62 'headers': {'content-type': 'application/json'},
63 'content': json.dumps({"id": "hook_id",
64 "merge_requests_events": True,
65 "url": "url"})}
66 else:
67 return {'status-code': 404}
68
69
70 @urlmatch(path=r'^/api/v4/projects/[^/]+$')
71 def mock_projects_get(url, request):
72 return {'status_code': 200,
73 'headers': {'content-type': 'application/json'},
74 'content': json.dumps({"id": "projectid"})}
75
76
77 @urlmatch(path=r'^/api/v4/projects/[^/]+/merge_requests')
78 def mock_projects_mergerequests_list_none(url, request):
79 return {'status_code': 200,
80 'headers': {'content-type': 'application/json'},
81 'content': '[]'}
82
83
84 @urlmatch(path=r'^/api/v4/projects/[^/]+/merge_requests/[^/]+')
85 def mock_projects_mergerequests_get_unreviewed_review(url, request):
86 return {'status_code': 200,
87 'headers': {'content-type': 'application/json'},
88 'content': json.dumps({'iid': 'mr_id',
89 'author': {'username': 'ReviewerX', 'id': 2001},
90 'created_at': 'Oct 29, 2017 2:37am',
91 'merge_status': 'can_be_merged',
92 'state': 'opened',
93 'upvotes': 0,
94 'web_url': 'http://gitlab.example.com/sample/mr/2001',
95 'work_in_progress': False})}
96
97
98 @urlmatch(path=r'^/api/v4/projects/[^/]+/merge_requests/[^/]+')
99 def mock_projects_mergerequests_get_wip_review(url, request):
100 return {'status_code': 200,
101 'headers': {'content-type': 'application/json'},
102 'content': json.dumps({'iid': 'mr_id',
103 'author': {'username': 'ReviewerX', 'id': 2001},
104 'created_at': 'Oct 29, 2017 2:37am',
105 'merge_status': 'can_be_merged',
106 'state': 'opened',
107 'upvotes': 0,
108 'web_url': 'http://gitlab.example.com/sample/mr/2001',
109 'work_in_progress': True})}
110
111
112 @urlmatch(path=r'^/api/v4/projects/[^/]+/merge_requests/[^/]+')
113 def mock_projects_mergerequests_get_waiting_review(url, request):
114 return {'status_code': 200,
115 'headers': {'content-type': 'application/json'},
116 'content': json.dumps({'iid': 'mr_id',
117 'author': {'username': 'ReviewerX', 'id': 2001},
118 'created_at': 'Oct 29, 2017 2:37am',
119 'merge_status': 'can_be_merged',
120 'state': 'opened',
121 'upvotes': 1,
122 'web_url': 'http://gitlab.example.com/sample/mr/2001',
123 'work_in_progress': False})}
124
125
126 @urlmatch(path=r'/api/v4/projects/[^/]+/merge_requests/[^/]+/award_emoji')
127 def mock_projects_mergerequests_awardemojis_list(url, request):
128 return {'status_code': 200,
129 'headers': {'content-type': 'application/json'},
130 'content': json.dumps([{'id': 'id',
131 'name': 'thumbsup',
132 'user': {'username': 'ReviewerX'}}])}
133
134
135 @urlmatch(path=r'^/api/v4/projects/[^/]+/merge_requests/[^/]+')
136 def mock_projects_mergerequests_get_mergable_review(url, request):
137 return {'status_code': 200,
138 'headers': {'content-type': 'application/json'},
139 'content': json.dumps({'iid': 'mr_id',
140 'author': {'username': 'ReviewerX', 'id': 2001},
141 'created_at': 'Oct 29, 2017 2:37am',
142 'merge_status': 'can_be_merged',
143 'state': 'opened',
144 'upvotes': 2,
145 'web_url': 'http://gitlab.example.com/sample/mr/2001',
146 'work_in_progress': False})}
147
148
149 @urlmatch(path=r'^/api/v4/projects/[^/]+/merge_requests/[^/]+')
150 def mock_projects_mergerequests_get_conflicted_review(url, request):
151 return {'status_code': 200,
152 'headers': {'content-type': 'application/json'},
153 'content': json.dumps({'iid': 'mr_id',
154 'author': {'username': 'ReviewerX', 'id': 2001},
155 'created_at': 'Oct 29, 2017 2:37am',
156 'merge_status': 'merge_conflicts',
157 'state': 'opened',
158 'upvotes': 2,
159 'web_url': 'http://gitlab.example.com/sample/mr/2001',
160 'work_in_progress': False})}
161
162
163 @urlmatch(path=r'^/api/v4/projects/[^/]+/merge_requests$', query='state=opened')
164 def mock_projects_mergerequests_list_unreviewed_review(url, request):
165 return {'status_code': 200,
166 'headers': {'content-type': 'application/json'},
167 'content': json.dumps([{'iid': 'mr_id',
168 'author': {'username': 'ReviewerX', 'id': 2001},
169 'created_at': 'Oct 29, 2017 2:37am',
170 'merge_status': 'can_be_merged',
171 'state': 'opened',
172 'upvotes': 0,
173 'web_url': 'http://gitlab.example.com/sample/mr/2001',
174 'work_in_progresso': False}])}
175
176
177 @urlmatch(path=r'^/api/v4/projects/[^/]+/merge_requests', query='state=reopened')
178 def mock_projects_mergerequests_list_reopened(url, request):
179 return {'status_code': 200,
180 'headers': {'content-type': 'application/json'},
181 'content': json.dumps([])}
182
183
184 @urlmatch(path=r'^/api/v4/projects/[^/]+/hooks/[^/]+$')
185 def mock_projects_hooks_get_already_watching(url, request):
186 return {'status_code': 200,
187 'headers': {'content-type': 'application/json'},
188 'content': json.dumps({'id': 'hook_id',
189 'merge_requests_events': True,
190 'url': 'https://webhook.errbot.com:3142/margebot/room1,room2,room3'})}
191
192
193 @urlmatch(path=r'^/api/v4/projects/[^/]+/hooks$')
194 def mock_projects_hooks_list_already_watching(url, request):
195 return {'status_code': 200,
196 'headers': {'content-type': 'application/json'},
197 'content': json.dumps([{'id': 'hook_id',
198 'merge_requests_events': True,
199 'url': 'https://webhook.errbot.com:3142/margebot/room1,room2,room3'}])}
200
201
202 class TestMarge:
203 """
204 Margebot Tests
205
206 Run 'py.test' from the margebot root directory
207 """
208
209 extra_plugin_dir = "./plugins/"
210 loglevel = logging.INFO
211
212 @pytest.fixture(autouse=True)
213 def no_requests(self, monkeypatch):
214 monkeypatch.setattr(requests.sessions.Session, 'request', None)
215
216 @pytest.fixture
217 def margebot(self, testbot, monkeypatch, mocker):
218 testbot.push_message("!plugin config Marge {'CHATROOM_HOST': 'conference.test.com', 'GITLAB_HOST': 'gitlab.test.com', 'GITLAB_ADMIN_TOKEN': 'fake-token', 'CRONTAB': '0 * * * *', 'VERIFY_SSL': True, 'CRONTAB_SOAK_HOURS': 1, 'WEBHOOK_URL': 'https://webhook.errbot.com:3142/margebot'}")
219 testbot.pop_message()
220 testbot.push_message("!plugin config Webserver {'HOST': '0.0.0.0', 'PORT':3141, 'SSL': {'certificate': '', 'enabled': False, 'host': '0.0.0.0', 'key': '', 'port': 3142}}")
221 testbot.pop_message()
222
223 def mock_get(self, key):
224 return {}
225 monkeypatch.setattr(errbot.storage.StoreMixin, '__getitem__', mock_get, raising=False)
226
227 def mock_keys(self):
228 return ['OPEN_MRS']
229 monkeypatch.setattr(errbot.storage.StoreMixin, 'keys', mock_keys, raising=False)
230
231 return testbot
232
233 @pytest.fixture
234 def margebot_one_review(self, margebot, monkeypatch):
235
236 def mock_get(self, key):
237 return {(1001, 2001, 'room1,room2'): True} # [(project, iid, notify_rooms)]
238
239 monkeypatch.setattr(errbot.storage.StoreMixin, '__getitem__', mock_get, raising=False)
240 return margebot
241
242 # ============================================================================
243
244 @pytest.mark.parametrize("rdelta,expected", [
245 (relativedelta(days=1), "1 day ago"),
246 (relativedelta(days=2), "2 days ago"),
247 (relativedelta(days=1, hours=1), "1 day, 1 hour ago"),
248 (relativedelta(days=2, hours=2), "2 days, 2 hours ago"),
249 (relativedelta(hours=2), "2 hours ago"),
250 (relativedelta(days=1, minutes=23), "1 day, 23 minutes ago"),
251 (relativedelta(minutes=2), "2 minutes ago"),
252 (relativedelta(minutes=1), "1 minute ago"),
253 (relativedelta(minutes=0), "now"), ])
254 def test_deltastr(self, rdelta, expected):
255 now = datetime.now()
256 tdelta = (now + rdelta) - now
257 assert deltastr(tdelta) == expected
258
259 def test_help(self, margebot):
260 margebot.push_message('!help')
261 help_message = margebot.pop_message()
262 assert "Marge" in help_message
263 assert '!reviews' in help_message
264 assert '!watchrepo' in help_message
265
266 def test_webstatus(self, margebot):
267 margebot.push_message('!webstatus')
268 assert 'margebot/<rooms>' in margebot.pop_message()
269
270 def test_watchrepo(self, margebot):
271 with HTTMock(mock_projects_get,
272 mock_projects_hooks_list,
273 mock_projects_mergerequests_list_none):
274 margebot.push_message('!watchrepo group/new_repo room1,room2,room3')
275 pm = margebot.pop_message()
276 assert 'Now watching for new MRs in the group/new_repo repo to the room1,room2,room3 room(s)' in pm
277 assert 'No open MRs were found in the repo.' in pm
278
279 def test_watchrepo_already_watching_repo(self, margebot):
280 with HTTMock(mock_projects_hooks_list_already_watching,
281 mock_projects_get):
282 margebot.push_message('!watchrepo group/new_repo room1,room2,room3')
283 pm = margebot.pop_message()
284 assert 'Already reporting group/new_repo MRs to the room1,room2,room3 room(s)' in pm
285
286 def test_watchrepo_updating_roomlist(self, margebot):
287 with HTTMock(mock_projects_hooks_list_already_watching,
288 mock_projects_hooks_get_already_watching,
289 mock_projects_get):
290 margebot.push_message('!watchrepo group/new_repo room4,room5,room6')
291 pm = margebot.pop_message()
292 assert 'Updating room list for group/new_repo MRs from room1,room2,room3 to room4,room5,room6' in pm
293
294 def test_watchrepo_existing_mr(self, margebot):
295 with HTTMock(mock_projects_get,
296 mock_projects_hooks_list,
297 mock_projects_mergerequests_list_unreviewed_review,
298 mock_projects_mergerequests_list_reopened):
299 margebot.push_message('!watchrepo sample/mr room1,room2,room3')
300 pm = margebot.pop_message()
301 assert 'Now watching for new MRs in the sample/mr repo to the room1,room2,room3 room(s)' in pm
302 assert '1 open MR was found in the repo.' in pm
303
304 def test_gitlab_hook(self, margebot):
305 request = json.dumps({'event_type': 'merge_request',
306 'object_attributes': {
307 'state': 'opened',
308 'work_in_progress': '',
309 'title': 'title',
310 'author_id': 'author_id',
311 'target_project_id': 'project_id',
312 'id': 'id',
313 'iid': 'iid'},
314 'project': {
315 'homepage': 'url'}})
316 with HTTMock(mock_users_get_author_id):
317 margebot.push_message("!webhook test /margebot/room1,room2 " + request)
318 assert 'Hi there ! author_id username has opened a new MR: \"title\"\nurl/merge_requests/iid' in margebot.pop_message()
319 margebot.push_message('!reviews')
320 assert 'Hi there ! author_id username has opened a new MR: \"title\"\nurl/merge_requests/iid' in margebot.pop_message()
321
322 def test_gitlab_hook_never_close(self, margebot):
323 request = json.dumps({'event_type': 'merge_request',
324 'object_attributes': {
325 'state': 'opened',
326 'work_in_progress': '',
327 'title': 'title',
328 'author_id': 'author_id',
329 'target_project_id': 'project_id',
330 'id': 'id',
331 'iid': 'iid'},
332 'project': {
333 'homepage': 'url'},
334 'labels': [{'title': 'never-close'}]})
335 with HTTMock(mock_users_get_author_id):
336 margebot.push_message("!webhook test /margebot/room1,room2 " + request)
337 assert 'Status code : 200' in margebot.pop_message()
338 margebot.push_message('!reviews')
339 assert 'Hi gbin: I found no open MRs for you.' in margebot.pop_message()
340
341 def test_gitlab_hook_never_abandoned(self, margebot):
342 request = json.dumps({'event_type': 'merge_request',
343 'object_attributes': {
344 'state': 'opened',
345 'work_in_progress': '',
346 'title': 'title',
347 'author_id': 'author_id',
348 'target_project_id': 'project_id',
349 'id': 'id',
350 'iid': 'iid'},
351 'project': {
352 'homepage': 'url'},
353 'labels': [{'title': 'abandoned'}]})
354 with HTTMock(mock_users_get_author_id):
355 margebot.push_message("!webhook test /margebot/room1,room2 " + request)
356 assert 'Status code : 200' in margebot.pop_message()
357 margebot.push_message('!reviews')
358 assert 'Hi gbin: I found no open MRs for you.' in margebot.pop_message()
359
360 def test_gitlab_hook_wip(self, margebot):
361 request = json.dumps({'event_type': 'merge_request',
362 'object_attributes': {
363 'state': 'opened',
364 'work_in_progress': 'true',
365 'title': 'title',
366 'author_id': 'author_id',
367 'target_project_id': 'project_id',
368 'id': 'id',
369 'iid': 'iid'},
370 'project': {
371 'homepage': 'url'}})
372 with HTTMock(mock_users_get_author_id):
373 margebot.push_message("!webhook test /margebot/room1,room2 " + request)
374 assert 'Hi there ! author_id username has opened a new WIP MR: \"title\"\nurl/merge_requests/iid' in margebot.pop_message()
375 margebot.push_message('!reviews')
376 assert 'Hi there ! author_id username has opened a new WIP MR: \"title\"\nurl/merge_requests/iid' in margebot.pop_message()
377
378 def test_gitlab_hook_unexpected_user(self, margebot):
379 request = json.dumps({'event_type': 'merge_request',
380 'object_attributes': {
381 'state': 'opened',
382 'work_in_progress': '',
383 'title': 'title',
384 'author_id': '(missing)',
385 'target_project_id': 'project_id',
386 'id': 'id',
387 'iid': 'iid'},
388 'project': {
389 'homepage': 'url'}})
390
391 with HTTMock(mock_users_get_unexpected):
392 margebot.push_message("!webhook test /margebot/room1,room2 " + request)
393 assert 'Hi there ! (missing) has opened a new MR: \"title\"\nurl/merge_requests/iid' in margebot.pop_message()
394 margebot.push_message('!reviews')
395 assert 'Hi there ! (missing) has opened a new MR: \"title\"\nurl/merge_requests/iid' in margebot.pop_message()
396
397 def test_gitlab_hook_unexpected_event_type(self, margebot, caplog):
398 request = json.dumps({'event_type': 'not_merge_request',
399 'object_attributes': {
400 'state': 'opened',
401 'work_in_progress': '',
402 'title': 'title',
403 'author_id': 'author_id',
404 'target_project_id': 'project_id',
405 'id': 'id',
406 'iid': 'iid'},
407 'project': {
408 'homepage': 'url'}})
409
410 with HTTMock(mock_users_search_gbin):
411 margebot.push_message("!webhook test /margebot/room1,room2 " + request)
412 margebot.pop_message()
413 margebot.push_message('!reviews')
414 assert 'Hi gbin: I found no open MRs for you.' in margebot.pop_message()
415 assert 'unexpecting event_type: not_merge_request' in caplog.text # Has to be at end of method
416
417 def test_nothing_to_review(self, margebot):
418 with HTTMock(mock_users_search_gbin):
419 margebot.push_message('!reviews')
420 assert 'Hi gbin: I found no open MRs for you.' in margebot.pop_message()
421
422 def test_get_one_open_mr(self, margebot_one_review):
423 with HTTMock(mock_users_search_gbin,
424 mock_projects_get,
425 mock_projects_mergerequests_get_unreviewed_review):
426 margebot_one_review.push_message('!reviews')
427 output = margebot_one_review.pop_message()
428 assert 'These MRs need some attention' in output
429 assert 'http://gitlab.example.com/sample/mr/2001' in output
430 assert 'No upvotes, please review.' in output
431
432 def test_get_one_wip_mr(self, margebot_one_review):
433 with HTTMock(mock_users_search_gbin,
434 mock_projects_get,
435 mock_projects_mergerequests_get_wip_review):
436 margebot_one_review.push_message('!reviews')
437 output = margebot_one_review.pop_message()
438 assert 'These MRs need some attention' in output
439 assert 'http://gitlab.example.com/sample/mr/2001' in output
440 assert 'No upvotes, please review but still WIP.' in output
441
442 def test_get_one_waiting_mr(self, margebot_one_review):
443 with HTTMock(mock_users_search_gbin,
444 mock_projects_get,
445 mock_projects_mergerequests_awardemojis_list,
446 mock_projects_mergerequests_get_waiting_review):
447 margebot_one_review.push_message('!reviews')
448 output = margebot_one_review.pop_message()
449 assert 'These MRs need some attention' in output
450 assert 'http://gitlab.example.com/sample/mr/2001' in output
451 assert 'Approved by ReviewerX and waiting for another upvote.' in output
452
453 def test_get_one_mergable_mr(self, margebot_one_review):
454 with HTTMock(mock_users_search_gbin,
455 mock_projects_get,
456 mock_projects_mergerequests_get_mergable_review):
457 margebot_one_review.push_message('!reviews')
458 output = margebot_one_review.pop_message()
459 assert 'These MRs need some attention' in output
460 assert 'http://gitlab.example.com/sample/mr/2001' in output
461 assert 'Has 2+ upvotes and could be merged in now.' in output
462
463 def test_get_one_conflicted_mr(self, margebot_one_review):
464 with HTTMock(mock_users_search_gbin,
465 mock_projects_get,
466 mock_projects_mergerequests_get_conflicted_review):
467 margebot_one_review.push_message('!reviews')
468 output = margebot_one_review.pop_message(timeout=1)
469 assert 'These MRs need some attention' in output
470 assert 'http://gitlab.example.com/sample/mr/2001' in output
471 assert 'Has 2+ upvotes and could be merged in now except there are merge conflicts.' in output
472
473 def test_crontab_hook(self, margebot_one_review, monkeypatch, mocker):
474 plugin = margebot_one_review._bot.plugin_manager.get_plugin_obj_by_name('Marge')
475
476 def mock_rooms():
477 return [mocker.MagicMock(node='room1'), mocker.MagicMock(node='room2')]
478 monkeypatch.setattr(plugin, 'rooms', mock_rooms)
479
480 with HTTMock(mock_projects_get,
481 mock_projects_mergerequests_get_unreviewed_review):
482 plugin.crontab_hook("unused")
483 output = margebot_one_review.pop_message()
484 assert 'These MRs need some attention' in output
485 assert 'http://gitlab.example.com/sample/mr/2001 (opened' in output
486
487 def test_margebot_unknown_command(self, margebot):
488 margebot.push_message('Margebot, alacazam')
489 with pytest.raises(Empty):
490 margebot.pop_message()