687500706b01171e0fafc97f5765c6db6d5a15df
[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': {'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': {'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': {'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': {'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': {'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': {'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({'object_kind': '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_wip(self, margebot):
323 request = json.dumps({'object_kind': 'merge_request',
324 'object_attributes': {
325 'state': 'opened',
326 'work_in_progress': 'true',
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 with HTTMock(mock_users_get_author_id):
335 margebot.push_message("!webhook test /margebot/room1,room2 " + request)
336 assert 'Hi there ! author_id username has opened a new WIP MR: \"title\"\nurl/merge_requests/iid' in margebot.pop_message()
337 margebot.push_message('!reviews')
338 assert 'Hi there ! author_id username has opened a new WIP MR: \"title\"\nurl/merge_requests/iid' in margebot.pop_message()
339
340 def test_gitlab_hook_unexpected_user(self, margebot):
341 request = json.dumps({'object_kind': 'merge_request',
342 'object_attributes': {
343 'state': 'opened',
344 'work_in_progress': '',
345 'title': 'title',
346 'author_id': '(missing)',
347 'target_project_id': 'project_id',
348 'id': 'id',
349 'iid': 'iid'},
350 'project': {
351 'homepage': 'url'}})
352
353 with HTTMock(mock_users_get_unexpected):
354 margebot.push_message("!webhook test /margebot/room1,room2 " + request)
355 assert 'Hi there ! (missing) has opened a new MR: \"title\"\nurl/merge_requests/iid' in margebot.pop_message()
356 margebot.push_message('!reviews')
357 assert 'Hi there ! (missing) has opened a new MR: \"title\"\nurl/merge_requests/iid' in margebot.pop_message()
358
359 def test_gitlab_hook_unexpected_object_kind(self, margebot, caplog):
360 request = json.dumps({'object_kind': 'not_merge_request',
361 'object_attributes': {
362 'state': 'opened',
363 'work_in_progress': '',
364 'title': 'title',
365 'author_id': 'author_id',
366 'target_project_id': 'project_id',
367 'id': 'id',
368 'iid': 'iid'},
369 'project': {
370 'homepage': 'url'}})
371
372 with HTTMock(mock_users_search_gbin):
373 margebot.push_message("!webhook test /margebot/room1,room2 " + request)
374 margebot.pop_message()
375 margebot.push_message('!reviews')
376 assert 'Hi gbin: I found no open MRs for you.' in margebot.pop_message()
377 assert 'unexpecting object_kind: not_merge_request' in caplog.text # Has to be at end of method
378
379 def test_nothing_to_review(self, margebot):
380 with HTTMock(mock_users_search_gbin):
381 margebot.push_message('!reviews')
382 assert 'Hi gbin: I found no open MRs for you.' in margebot.pop_message()
383
384 def test_get_one_open_mr(self, margebot_one_review):
385 with HTTMock(mock_users_search_gbin,
386 mock_projects_get,
387 mock_projects_mergerequests_get_unreviewed_review):
388 margebot_one_review.push_message('!reviews')
389 output = margebot_one_review.pop_message()
390 assert 'These MRs need some attention' in output
391 assert 'http://gitlab.example.com/sample/mr/2001' in output
392 assert 'No upvotes, please review.' in output
393
394 def test_get_one_wip_mr(self, margebot_one_review):
395 with HTTMock(mock_users_search_gbin,
396 mock_projects_get,
397 mock_projects_mergerequests_get_wip_review):
398 margebot_one_review.push_message('!reviews')
399 output = margebot_one_review.pop_message()
400 assert 'These MRs need some attention' in output
401 assert 'http://gitlab.example.com/sample/mr/2001' in output
402 assert 'No upvotes, please review but still WIP.' in output
403
404 def test_get_one_waiting_mr(self, margebot_one_review):
405 with HTTMock(mock_users_search_gbin,
406 mock_projects_get,
407 mock_projects_mergerequests_awardemojis_list,
408 mock_projects_mergerequests_get_waiting_review):
409 margebot_one_review.push_message('!reviews')
410 output = margebot_one_review.pop_message()
411 assert 'These MRs need some attention' in output
412 assert 'http://gitlab.example.com/sample/mr/2001' in output
413 assert 'Approved by ReviewerX and waiting for another upvote.' in output
414
415 def test_get_one_mergable_mr(self, margebot_one_review):
416 with HTTMock(mock_users_search_gbin,
417 mock_projects_get,
418 mock_projects_mergerequests_get_mergable_review):
419 margebot_one_review.push_message('!reviews')
420 output = margebot_one_review.pop_message()
421 assert 'These MRs need some attention' in output
422 assert 'http://gitlab.example.com/sample/mr/2001' in output
423 assert 'Has 2+ upvotes and could be merged in now.' in output
424
425 def test_get_one_conflicted_mr(self, margebot_one_review):
426 with HTTMock(mock_users_search_gbin,
427 mock_projects_get,
428 mock_projects_mergerequests_get_conflicted_review):
429 margebot_one_review.push_message('!reviews')
430 output = margebot_one_review.pop_message(timeout=1)
431 assert 'These MRs need some attention' in output
432 assert 'http://gitlab.example.com/sample/mr/2001' in output
433 assert 'Has 2+ upvotes and could be merged in now except there are merge conflicts.' in output
434
435 def test_crontab_hook(self, margebot_one_review, monkeypatch, mocker):
436 plugin = margebot_one_review._bot.plugin_manager.get_plugin_obj_by_name('Marge')
437
438 def mock_rooms():
439 return [mocker.MagicMock(node='room1'), mocker.MagicMock(node='room2')]
440 monkeypatch.setattr(plugin, 'rooms', mock_rooms)
441
442 with HTTMock(mock_projects_get,
443 mock_projects_mergerequests_get_unreviewed_review):
444 plugin.crontab_hook("unused")
445 output = margebot_one_review.pop_message()
446 assert 'These MRs need some attention' in output
447 assert 'http://gitlab.example.com/sample/mr/2001 (opened' in output
448
449 def test_margebot_unknown_command(self, margebot):
450 margebot.push_message('Margebot, alacazam')
451 with pytest.raises(Empty):
452 margebot.pop_message()