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
12 from datetime
import datetime
15 from queue
import Empty
17 from dateutil
.relativedelta
import relativedelta
19 from errbot
.backends
.test
import testbot
# pylint: disable=unused-import
22 from httmock
import urlmatch
, HTTMock
23 from plugins
.marge
import deltastr
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})}
36 @urlmatch(path
=r
'/api/v4/users/[^/\?]+$')
37 def mock_users_get_unexpected(url
, request
):
38 return {'status_code': 404}
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}])}
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'},
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,
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,
67 return {'status-code': 404}
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"})}
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'},
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',
94 'web_url': 'http://gitlab.example.com/sample/mr/2001',
95 'work_in_progress': False})}
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',
108 'web_url': 'http://gitlab.example.com/sample/mr/2001',
109 'work_in_progress': True})}
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',
122 'web_url': 'http://gitlab.example.com/sample/mr/2001',
123 'work_in_progress': False})}
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',
132 'user': {'username': 'ReviewerX'}}])}
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',
145 'web_url': 'http://gitlab.example.com/sample/mr/2001',
146 'work_in_progress': False})}
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',
159 'web_url': 'http://gitlab.example.com/sample/mr/2001',
160 'work_in_progress': False})}
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',
173 'web_url': 'http://gitlab.example.com/sample/mr/2001',
174 'work_in_progresso': False}])}
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([])}
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'})}
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'}])}
206 Run 'py.test' from the margebot root directory
209 extra_plugin_dir
= "./plugins/"
210 loglevel
= logging
.INFO
212 @pytest.fixture(autouse
=True)
213 def no_requests(self
, monkeypatch
):
214 monkeypatch
.setattr(requests
.sessions
.Session
, 'request', None)
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()
223 def mock_get(self
, key
):
225 monkeypatch
.setattr(errbot
.storage
.StoreMixin
, '__getitem__', mock_get
, raising
=False)
229 monkeypatch
.setattr(errbot
.storage
.StoreMixin
, 'keys', mock_keys
, raising
=False)
234 def margebot_one_review(self
, margebot
, monkeypatch
):
236 def mock_get(self
, key
):
237 return {(1001, 2001, 'room1,room2'): True} # [(project, iid, notify_rooms)]
239 monkeypatch
.setattr(errbot
.storage
.StoreMixin
, '__getitem__', mock_get
, raising
=False)
242 # ============================================================================
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
):
256 tdelta
= (now
+ rdelta
) - now
257 assert deltastr(tdelta
) == expected
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
266 def test_webstatus(self
, margebot
):
267 margebot
.push_message('!webstatus')
268 assert 'margebot/<rooms>' in margebot
.pop_message()
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
279 def test_watchrepo_already_watching_repo(self
, margebot
):
280 with
HTTMock(mock_projects_hooks_list_already_watching
,
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
286 def test_watchrepo_updating_roomlist(self
, margebot
):
287 with
HTTMock(mock_projects_hooks_list_already_watching
,
288 mock_projects_hooks_get_already_watching
,
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
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
304 def test_gitlab_hook(self
, margebot
):
305 request
= json
.dumps({'object_kind': 'merge_request',
306 'object_attributes': {
308 'work_in_progress': '',
310 'author_id': 'author_id',
311 'target_project_id': 'project_id',
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()
322 def test_gitlab_hook_wip(self
, margebot
):
323 request
= json
.dumps({'object_kind': 'merge_request',
324 'object_attributes': {
326 'work_in_progress': 'true',
328 'author_id': 'author_id',
329 'target_project_id': 'project_id',
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()
340 def test_gitlab_hook_unexpected_user(self
, margebot
):
341 request
= json
.dumps({'object_kind': 'merge_request',
342 'object_attributes': {
344 'work_in_progress': '',
346 'author_id': '(missing)',
347 'target_project_id': 'project_id',
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()
359 def test_gitlab_hook_unexpected_object_kind(self
, margebot
, caplog
):
360 request
= json
.dumps({'object_kind': 'not_merge_request',
361 'object_attributes': {
363 'work_in_progress': '',
365 'author_id': 'author_id',
366 'target_project_id': 'project_id',
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
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()
384 def test_get_one_open_mr(self
, margebot_one_review
):
385 with
HTTMock(mock_users_search_gbin
,
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
394 def test_get_one_wip_mr(self
, margebot_one_review
):
395 with
HTTMock(mock_users_search_gbin
,
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
404 def test_get_one_waiting_mr(self
, margebot_one_review
):
405 with
HTTMock(mock_users_search_gbin
,
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
415 def test_get_one_mergable_mr(self
, margebot_one_review
):
416 with
HTTMock(mock_users_search_gbin
,
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
425 def test_get_one_conflicted_mr(self
, margebot_one_review
):
426 with
HTTMock(mock_users_search_gbin
,
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
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')
439 return [mocker
.MagicMock(node
='room1'), mocker
.MagicMock(node
='room2')]
440 monkeypatch
.setattr(plugin
, 'rooms', mock_rooms
)
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
449 def test_margebot_unknown_command(self
, margebot
):
450 margebot
.push_message('Margebot, alacazam')
451 with pytest
.raises(Empty
):
452 margebot
.pop_message()