Coverage for CIResults/tests/test_bugtrackers.py: 100%
1274 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-09 12:54 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-09 12:54 +0000
1from unittest.mock import call, patch, MagicMock, PropertyMock
2from django.test import TestCase, TransactionTestCase
3from django.core.exceptions import MultipleObjectsReturned
4from django.db import IntegrityError
5from dateutil import parser as dateparser
6from django.utils import timezone
8from CIResults.models import BugTracker, Bug, Person, BugTrackerAccount, BugComment, ReplicationScript
9from CIResults.bugtrackers import BugTrackerCommon, Bugzilla, Untracked, Jira, GitLab, BugCommentTransport
10from CIResults.serializers import serialize_bug
12from collections import namedtuple
13from jira.exceptions import JIRAError
14from jira.client import ResultList
15import urllib.parse
16import xmlrpc.client
17import requests
18import datetime
19from datetime import timedelta
20import pytz
21import copy
24class BugTrackerCommonTests(TestCase):
25 fields = {'title': "Kwyjibo",
26 'status': "D'oh",
27 'description': "A big, dumb, balding North American ape.",
28 'product': "The Simpsons",
29 'platforms': "TV",
30 'priority': "High",
31 'component': "Homer"}
33 @patch('CIResults.models.BugTrackerAccount.objects.filter', return_value=[BugTrackerAccount(user_id="1"),
34 BugTrackerAccount(user_id="2"),
35 BugTrackerAccount(user_id="3")])
36 def test_account_cached(self, filter_mocked):
37 db_tracker = BugTracker(name="Tracker1", public=True)
38 common = BugTrackerCommon(db_tracker)
39 accounts = common.accounts_cached
41 filter_mocked.assert_called_with(tracker=db_tracker)
42 self.assertEqual(accounts, {"1": filter_mocked.return_value[0],
43 "2": filter_mocked.return_value[1],
44 "3": filter_mocked.return_value[2]})
46 # Check that the corresponding account is returned if it exists
47 def test_find_or_create_account__existing(self):
48 user_id = "my id"
50 db_tracker = BugTracker(name="Tracker1", public=True)
51 common = BugTrackerCommon(db_tracker)
52 common.accounts_cached = {user_id: MagicMock()}
54 account = common.find_or_create_account(user_id)
55 self.assertEqual(account, common.accounts_cached[user_id])
56 self.assertEqual(Person.objects.all().count(), 0)
57 self.assertEqual(BugTrackerAccount.objects.all().count(), 0)
59 # Check that a new account is created when it does not exist yet, then
60 # that subsequent changes get registered
61 def test_find_or_create_account(self):
62 user_id = "my id"
63 name = "John Doe"
64 email = "me@email.com"
66 db_tracker = BugTracker.objects.create(name="Tracker1", public=True)
67 common = BugTrackerCommon(db_tracker)
68 account = common.find_or_create_account(user_id, email=email, full_name=name)
70 self.assertEqual(account.tracker, db_tracker)
71 self.assertEqual(account.user_id, user_id)
72 self.assertEqual(account.is_developer, False)
73 self.assertEqual(account.person.full_name, name)
74 self.assertEqual(account.person.email, email)
76 name2 = "John Doe 2"
77 common.find_or_create_account(user_id, email=email, full_name=name2)
78 account = BugTrackerAccount.objects.get(user_id=user_id)
79 self.assertEqual(account.person.full_name, name2)
80 self.assertEqual(account.person.email, email)
82 email2 = "me2@email.com"
83 common.find_or_create_account(user_id, email=email2, full_name=name)
84 account = BugTrackerAccount.objects.get(user_id=user_id)
85 self.assertEqual(account.person.full_name, name)
86 self.assertEqual(account.person.email, email2)
88 def test_create_bug(self):
89 db_tracker = BugTracker.objects.create(name="Tracker1", project="FOO", public=True)
90 common = BugTrackerCommon(db_tracker)
92 bug = Bug.objects.create(tracker=db_tracker, **self.fields)
93 common.create_bug_from_json = MagicMock()
94 common.create_bug(bug)
95 out_fields = common.create_bug_from_json.call_args[0][0]
96 for field in self.fields:
97 self.assertEqual(self.fields[field], out_fields[field])
99 def test_create_bug_existing(self):
100 db_tracker = BugTracker.objects.create(name="Tracker1", project="FOO", public=True)
101 common = BugTrackerCommon(db_tracker)
103 bug = Bug.objects.create(tracker=db_tracker, bug_id=10, **self.fields)
104 common.create_bug_from_json = MagicMock()
105 with self.assertRaises(ValueError):
106 common.create_bug(bug)
108 def test_create_bug_no_project(self):
109 db_tracker = BugTracker.objects.create(name="Tracker1", public=True)
110 common = BugTrackerCommon(db_tracker)
112 bug = Bug.objects.create(tracker=db_tracker, **self.fields)
113 common.create_bug_from_json = MagicMock()
114 with self.assertRaises(ValueError):
115 common.create_bug(bug)
117 def test__parse_custom_field(self):
118 db_tracker = BugTracker.objects.create(name="Tracker1", public=True)
119 common = BugTrackerCommon(db_tracker)
121 self.assertEqual('[1, 2, 3]', common._parse_custom_field([1, 2, 3], to_str=True))
122 self.assertEqual([1, 2, 3], common._parse_custom_field([1, 2, 3], to_str=False))
125class SandboxMock():
126 @classmethod
127 def get_or_create_instance(cls, script):
128 return cls(script)
130 def __init__(self, script):
131 self.script = script
133 def call_user_function(self, fn, kwargs):
134 code = compile(self.script, "<user script>", 'exec')
135 sandbox_globals = {}
136 exec(code, sandbox_globals)
137 return sandbox_globals[fn](**kwargs)
140@patch('CIResults.bugtrackers.Client', SandboxMock)
141class BugTrackerReplicationTests(TestCase):
142 def setUp(self):
143 self.title = "We are the knights who say..."
144 self.description = "Ni!"
145 self.script = """\
146def replication_check(src_bug, dest_bug):
147 if int(src_bug['bug_id']) % 2 == 0:
148 return {"set_fields": {'description': src_bug['description'],'title': src_bug['title']},
149 "add_comments": ["apple", "pie"]}
150 else:
151 return {}
152 """
153 self.db_tracker = BugTracker.objects.create(name="Tracker1", project="TEST", tracker_type="bugzilla",
154 url="http://bar", public=True)
155 self.rep_tracker = BugTracker.objects.create(name="Tracker2", tracker_type="jira", url="http://foo",
156 project="TEST2", public=True)
157 self.tracker = Untracked(self.db_tracker)
158 self.tracker2 = Untracked(self.rep_tracker)
160 self.rp = ReplicationScript.objects.create(source_tracker=self.db_tracker,
161 destination_tracker=self.rep_tracker,
162 script=self.script,
163 enabled=True,
164 name="BAR")
166 self.bug = Bug.objects.create(tracker=self.db_tracker, bug_id=2,
167 title=self.title, description=self.description)
169 def get_mirrored_bug_if_created(self, bug, bug_id=4, comments=None, ret_mocks=False):
170 comments = comments if comments else []
171 with patch('CIResults.bugtrackers.Jira.create_bug_from_json', MagicMock()) as create_mock:
172 with patch('CIResults.bugtrackers.Jira.poll', autospec=True):
173 with patch('CIResults.bugtrackers.Jira.add_comment', MagicMock()) as add_comm_mock:
174 create_mock.return_value = bug_id
175 self.tracker.check_replication(bug, comments)
176 bug = Bug.objects.filter(parent=bug).first()
177 if ret_mocks:
178 return (bug, create_mock, add_comm_mock)
179 else:
180 return bug
182 def get_updated_bug(self, bug):
183 with patch('CIResults.bugtrackers.Jira.update_bug_from_json', MagicMock()) as upd_mock:
184 with patch('CIResults.bugtrackers.Jira.poll', autospec=True):
185 with patch('CIResults.bugtrackers.Jira.add_comment', MagicMock()) as add_comm_mock:
186 self.tracker.check_replication(bug, [])
187 return upd_mock, add_comm_mock
189 def test_tracker_check_replication(self):
190 client = MagicMock()
191 client.call_user_function = MagicMock()
192 client.call_user_function.return_value = {"set_fields": {}}
194 resp = self.tracker.tracker_check_replication([self.bug], self.rep_tracker, self.script, client, dryrun=True)
195 ser_bug = serialize_bug(self.bug)
196 client.call_user_function.assert_called_with("replication_check", kwargs={"src_bug": ser_bug,
197 "dest_bug": None})
198 self.assertEqual(resp[0]["operation"], "create")
200 def test_tracker_check_replication_update(self):
201 client = MagicMock()
202 bug = Bug.objects.create(tracker=self.rep_tracker, parent=self.bug)
204 client.call_user_function = MagicMock()
205 client.call_user_function.return_value = {"set_fields": {}}
207 resp = self.tracker.tracker_check_replication([self.bug], self.rep_tracker, self.script, client, dryrun=True)
208 ser_bug = serialize_bug(self.bug)
209 ser_dest_bug = serialize_bug(bug)
210 client.call_user_function.assert_called_with("replication_check", kwargs={"src_bug": ser_bug,
211 "dest_bug": ser_dest_bug})
212 self.assertEqual(resp[0]["operation"], "update")
214 def test_tracker_check_replication_invalid_bug(self):
215 client = MagicMock()
216 bug = Bug(tracker=self.db_tracker)
217 resp = self.tracker.tracker_check_replication([bug], self.rep_tracker, self.script, client, dryrun=True)
218 self.assertEqual(resp, [])
220 bug.parent = self.bug
221 bug.save()
223 resp = self.tracker.tracker_check_replication([bug], self.rep_tracker, self.script, client, dryrun=True)
224 self.assertEqual(resp, [])
226 def test_tracker_check_replication_client_error(self):
227 client = MagicMock()
228 client.call_user_function = MagicMock(side_effect=Exception())
229 client.call_user_function.return_value = {"set_fields": {}}
231 resp = self.tracker.tracker_check_replication([self.bug], self.rep_tracker, self.script, client, dryrun=True)
232 self.assertEqual(resp, [])
234 def test_check_replication(self):
235 m_bug, mock, _ = self.get_mirrored_bug_if_created(self.bug, ret_mocks=True)
236 self.assertIsNotNone(m_bug)
237 mock.assert_called_with({'description': self.description, 'title': self.title})
239 def test_check_replication_comments(self):
240 name = "Ada"
241 email = "foo@bar.com"
242 body = "this is a test"
243 body2 = "this is also a test"
244 person = Person.objects.create(full_name=name)
245 person2 = Person.objects.create(email=email)
246 account = BugTrackerAccount.objects.create(person=person, user_id="1",
247 tracker=self.db_tracker, is_developer=True)
248 account2 = BugTrackerAccount.objects.create(person=person2, user_id="2",
249 tracker=self.db_tracker, is_developer=True)
250 created = datetime.datetime.now()
251 created2 = datetime.datetime.now() + timedelta(days=1)
252 comment = BugComment.objects.create(bug=self.bug, account=account,
253 created_on=created, comment_id="1")
254 comment2 = BugComment.objects.create(bug=self.bug, account=account2,
255 created_on=created2, comment_id="2")
256 new_comments = [BugCommentTransport(comment, body), BugCommentTransport(comment2, body2)]
257 exp_comm = [{'author': name,
258 'created': str(created),
259 'body': body},
260 {'author': email,
261 'created': str(created2),
262 'body': body2}]
264 with patch('CIResults.bugtrackers.Client.call_user_function') as cuf_mock:
265 m_bug, mock, _ = self.get_mirrored_bug_if_created(self.bug, comments=new_comments, ret_mocks=True)
266 self.assertIsNotNone(m_bug)
267 src_bug_comments = cuf_mock.call_args[1]['kwargs']['src_bug']['new_comments']
268 self.assertEqual(src_bug_comments, exp_comm)
270 def test_check_replication_add_comments_string(self):
271 self.rp.delete()
272 script = """\
273def replication_check(src_bug, dest_bug):
274 if int(src_bug['bug_id']) % 2 == 0:
275 return {"set_fields": {'description': src_bug['description'],'title': src_bug['title']},
276 "add_comments": "Doh"}
277 else:
278 return {}
279 """
280 ReplicationScript.objects.create(source_tracker=self.db_tracker,
281 destination_tracker=self.rep_tracker,
282 script=script,
283 enabled=True,
284 name="BAR")
285 m_bug, mock, add_comm_mock = self.get_mirrored_bug_if_created(self.bug, ret_mocks=True)
286 self.assertIsNotNone(m_bug)
287 mock.assert_called_with({'description': self.description, 'title': self.title})
288 add_comm_mock.assert_called_with(m_bug, "Doh")
290 def test_check_replication_add_comments_list(self):
291 self.rp.delete()
292 script = """\
293def replication_check(src_bug, dest_bug):
294 if int(src_bug['bug_id']) % 2 == 0:
295 return {"set_fields": {'description': src_bug['description'],'title': src_bug['title']},
296 "add_comments": ["hello","world","foo"]}
297 else:
298 return {}
299 """
300 ReplicationScript.objects.create(source_tracker=self.db_tracker,
301 destination_tracker=self.rep_tracker,
302 script=script,
303 enabled=True)
305 m_bug, mock, add_comm_mock = self.get_mirrored_bug_if_created(self.bug, ret_mocks=True)
306 self.assertIsNotNone(m_bug)
307 mock.assert_called_with({'description': self.description, 'title': self.title})
308 add_comm_mock.assert_has_calls([call(m_bug, "hello"), call(m_bug, "world"), call(m_bug, "foo")])
310 def test_check_replication_add_comments_no_comment(self):
311 self.rp.delete()
312 script = """\
313def replication_check(src_bug, dest_bug):
314 if int(src_bug['bug_id']) % 2 == 0:
315 return {"set_fields": {'description': src_bug['description'],'title': src_bug['title']}}
316 else:
317 return {}
318 """
319 ReplicationScript.objects.create(source_tracker=self.db_tracker,
320 destination_tracker=self.rep_tracker,
321 script=script,
322 enabled=True)
324 m_bug, mock, add_comm_mock = self.get_mirrored_bug_if_created(self.bug, ret_mocks=True)
325 self.assertIsNotNone(m_bug)
326 mock.assert_called_with({'description': self.description, 'title': self.title})
327 add_comm_mock.assert_not_called()
329 def test_check_replication_db_fields_update(self):
330 self.rp.delete()
331 script = """\
332def replication_check(src_bug, dest_bug):
333 if int(src_bug['bug_id']) % 2 == 0:
334 return {"set_fields": {'description': src_bug['description'],'title': src_bug['title']},
335 "add_comments": "Doh",
336 "db_dest_fields_update": {'severity': 'High'},
337 "db_src_fields_update": {'priority': 'Low'}}
338 else:
339 return {}
340 """
341 ReplicationScript.objects.create(source_tracker=self.db_tracker,
342 destination_tracker=self.rep_tracker,
343 script=script,
344 enabled=True,
345 name="BAR")
346 m_bug = self.get_mirrored_bug_if_created(self.bug)
347 self.assertEqual(m_bug.severity, 'High')
348 self.assertEqual(self.bug.priority, 'Low')
349 m_bug.severity = None
350 self.bug.priority = None
351 m_bug.save()
352 _, _ = self.get_updated_bug(self.bug)
353 m_bug.refresh_from_db()
354 self.assertEqual(m_bug.severity, 'High')
355 self.assertEqual(self.bug.priority, 'Low')
357 def test_check_replication_db_fields_update_deprecated(self):
358 self.rp.delete()
359 script = """\
360def replication_check(src_bug, dest_bug):
361 if int(src_bug['bug_id']) % 2 == 0:
362 return {"set_fields": {'description': src_bug['description'],'title': src_bug['title']},
363 "add_comments": "Doh",
364 "db_fields_update": {'severity': 'High'}}
365 else:
366 return {}
367 """
368 ReplicationScript.objects.create(source_tracker=self.db_tracker,
369 destination_tracker=self.rep_tracker,
370 script=script,
371 enabled=True,
372 name="BAR")
373 m_bug = self.get_mirrored_bug_if_created(self.bug)
374 self.assertEqual(m_bug.severity, 'High')
375 m_bug.severity = None
376 m_bug.save()
377 _, _ = self.get_updated_bug(self.bug)
378 m_bug.refresh_from_db()
379 self.assertEqual(m_bug.severity, 'High')
381 def test_check_replication_db_fields_update_empty(self):
382 self.rp.delete()
383 script = """\
384def replication_check(src_bug, dest_bug):
385 if int(src_bug['bug_id']) % 2 == 0:
386 return {"set_fields": {'description': src_bug['description'],'title': src_bug['title']},
387 "add_comments": "Doh",
388 "db_dest_fields_update": {},
389 "db_src_fields_update": {}}
390 else:
391 return {}
392 """
393 ReplicationScript.objects.create(source_tracker=self.db_tracker,
394 destination_tracker=self.rep_tracker,
395 script=script,
396 enabled=True,
397 name="BAR")
398 with patch('CIResults.bugtrackers.Bug.update_from_dict') as upd_mock:
399 self.get_mirrored_bug_if_created(self.bug)
400 upd_mock.assert_called_with(None)
401 with patch('CIResults.bugtrackers.Bug.update_from_dict') as upd_mock:
402 _, _ = self.get_updated_bug(self.bug)
403 upd_mock.assert_called_with(None)
405 def test_check_replication_update(self):
406 new_bug = self.get_mirrored_bug_if_created(self.bug)
407 self.assertIsNotNone(new_bug)
408 upd_mock, _ = self.get_updated_bug(self.bug)
409 upd_mock.assert_called_with({'description': 'Ni!', 'title': 'We are the knights who say...'},
410 new_bug.bug_id)
412 @patch('CIResults.bugtrackers.BugTrackerCommon._replication_add_comments')
413 def test_check_replication_update_add_comments(self, add_comm_mock):
414 new_bug = self.get_mirrored_bug_if_created(self.bug)
415 self.assertIsNotNone(new_bug)
416 upd_mock, _ = self.get_updated_bug(self.bug)
417 upd_mock.assert_called_with({'description': 'Ni!', 'title': 'We are the knights who say...'},
418 new_bug.bug_id)
419 add_comm_mock.assert_called_with(new_bug, ["apple", "pie"])
421 def test_check_replication_update_error(self):
422 new_bug = self.get_mirrored_bug_if_created(self.bug)
423 self.assertIsNotNone(new_bug)
424 with patch('CIResults.bugtrackers.Jira.update_bug_from_json', MagicMock()) as upd_mock:
425 with patch('CIResults.bugtrackers.Jira.poll', autospec=True) as poll_mock:
426 upd_mock.side_effect = ValueError()
427 self.tracker.check_replication(self.bug, [])
428 poll_mock.assert_not_called()
430 @patch('CIResults.bugtrackers.Jira')
431 @patch('CIResults.bugtrackers.GitLab')
432 def test_check_replication_two_scripts(self, gitlab_mock, jira_mock):
433 dest_tracker = BugTracker.objects.create(name="TrackerFoo", tracker_type="gitlab", url="http://foo",
434 project="TESTFOO", public=True)
435 ReplicationScript.objects.create(source_tracker=self.db_tracker,
436 destination_tracker=dest_tracker,
437 script=self.script,
438 enabled=True,
439 name="FOO")
441 jira_mock.return_value.create_bug_from_json.return_value = 11
442 gitlab_mock.return_value.create_bug_from_json.return_value = 12
444 self.tracker.check_replication(self.bug, [])
445 bugs = Bug.objects.filter(parent=self.bug)
446 self.assertEqual(len(bugs), 2)
447 self.assertIsNotNone(bugs.get(tracker=self.rep_tracker))
448 self.assertIsNotNone(bugs.get(tracker=dest_tracker))
450 def test_check_invalid_replication(self):
451 with patch('CIResults.bugtrackers.Jira.create_bug_from_json', MagicMock()) as mock:
452 mock.side_effect = ValueError
453 self.tracker.check_replication(self.bug, [])
454 m_bug = Bug.objects.filter(parent=self.bug).first()
455 self.assertIsNone(m_bug)
457 def test_check_replication_already_mirrored(self):
458 new_bug = self.get_mirrored_bug_if_created(self.bug)
459 with patch('CIResults.bugtrackers.Jira.update_bug_from_json', MagicMock()) as upd_mock:
460 with patch('CIResults.bugtrackers.Jira.poll', autospec=True):
461 with patch('CIResults.bugtrackers.Jira.add_comment', MagicMock()):
462 self.tracker.check_replication(self.bug, [])
464 upd_mock.assert_called_with({'description': 'Ni!', 'title': 'We are the knights who say...'},
465 new_bug.bug_id)
466 try:
467 Bug.objects.get(parent=self.bug)
468 except MultipleObjectsReturned: # pragma: no cover
469 self.fail("New Bug shouldn't have been created") # pragma: no cover
471 def test_check_replication_replicated_bug(self):
472 m_bug = self.get_mirrored_bug_if_created(self.bug)
473 self.assertIsNotNone(m_bug)
474 m_bug2 = self.get_mirrored_bug_if_created(m_bug)
475 self.assertIsNone(m_bug2)
477 def test_check_replication_disabled(self):
478 self.rp.delete()
479 ReplicationScript.objects.create(source_tracker=self.db_tracker,
480 destination_tracker=self.rep_tracker,
481 script=self.script,
482 enabled=False)
483 with patch('CIResults.bugtrackers.Client.call_user_function', MagicMock()) as chk_mock:
484 self.assertIsNone(self.get_mirrored_bug_if_created(self.bug))
485 chk_mock.assert_not_called()
487 def test_check_replication_no_script(self):
488 self.rp.delete()
489 with patch('CIResults.bugtrackers.Client.call_user_function', MagicMock()) as chk_mock:
490 self.assertIsNone(self.get_mirrored_bug_if_created(self.bug))
491 chk_mock.assert_not_called()
493 def test_check_replication_no_match(self):
494 bug = Bug.objects.create(tracker=self.db_tracker, bug_id=1,
495 title=self.title, description=self.description)
496 self.assertIsNone(self.get_mirrored_bug_if_created(bug))
498 def test_check_replication_fail_save(self):
499 with patch('CIResults.models.Bug.save', MagicMock()) as val_mock:
500 val_mock.side_effect = IntegrityError("Non unique bug")
501 self.assertIsNone(self.get_mirrored_bug_if_created(self.bug))
503 def test_check_replication_no_fields(self):
504 with patch('CIResults.bugtrackers.Client.call_user_function', MagicMock()) as chk_mock:
505 chk_mock.return_value = None
506 self.assertIsNone(self.get_mirrored_bug_if_created(self.bug))
509class RequestsGetMock():
510 PRIVATE_TOKEN = "qwerttyzxcfdsapjdpfa"
511 BUG_ID = 2
512 BUG_CREATED_AT = '2018-10-04T11:20:48.531Z'
513 BUG_UPDATED_AT = '2018-11-28T13:24:13.325Z'
514 CREATOR_NAME = 'Creator Name'
515 ASSIGNEE_NAME = 'Assignee Name'
516 NOTE_ONE_ID = 83161
517 NOTE_ONE_BODY = "Still Alive"
518 NOTE_ONE_CREATOR_NAME = 'Note Creator'
519 NOTE_TWO_CREATED_AT = '2018-11-28T13:24:13.290Z'
520 NOTE_ONE_CREATED_AT = '2018-10-04T12:35:03.299Z'
521 NOTE_TWO_BODY = "Oh. Hi. So. How are you holding up? BECAUSE I'M A POTATO!"
522 BUG_TITLE = 'super bug title'
523 BUG_STATUS = 'opened'
524 BUG_DESCRIPTION = 'the cake is a lie'
526 BUG_PRODUCT = 'PrOdUcT'
527 BUG_COMPONENT = 'cOmPoNeNt'
528 BUG_PRIORITY = 'pRiOrItY'
529 BUG_SEVERITY = 'sEvErItY'
530 BUG_PLATFORM1 = 'pLaTfOrM 1'
531 BUG_PLATFORM2 = 'pLaTfOrM 2'
532 BUG_FEATURE1 = 'fEaTuRe 1'
533 BUG_FEATURE2 = 'fEaTuRe 2'
534 BUG_TARGET = 'bug_target'
535 BUG_MAP_SEVERITY = 'High'
537 BUG_LABELS = ['FOO', 'BAR', 'pRoDuCt::'+BUG_PRODUCT, 'CoMpOnEnT::'+BUG_COMPONENT, 'PrIoRiTy::'+BUG_PRIORITY,
538 'SeVeRiTy::'+BUG_SEVERITY, 'PlAtFoRm: '+BUG_PLATFORM1, 'PlAtFoRm: '+BUG_PLATFORM2,
539 'FeAtUrE: '+BUG_FEATURE1, 'FeAtUrE: '+BUG_FEATURE2]
540 RESPONSES = {
541 'https://gitlab.freedesktop.org/api/v4/projects/230/issues/2':
542 {
543 'id': 4674,
544 'iid': BUG_ID,
545 'project_id': 230,
546 'title': BUG_TITLE,
547 'description': BUG_DESCRIPTION,
548 'state': BUG_STATUS,
549 'created_at': BUG_CREATED_AT,
550 'updated_at': BUG_UPDATED_AT,
551 'closed_at': None,
552 'closed_by': None,
553 'labels': BUG_LABELS,
554 'author': {'id': 1127, 'name': CREATOR_NAME},
555 'assignee': {'id': 1128, 'name': ASSIGNEE_NAME},
556 'web_url': 'https://gitlab.freedesktop.org/patchwork-fdo/patchwork-fdo/issues/2'
557 },
558 'https://gitlab.freedesktop.org/api/v4/projects/230/issues/2/notes':
559 [
560 {
561 'id': NOTE_ONE_ID,
562 'body': NOTE_ONE_BODY,
563 'author': {'id': 1129, 'name': NOTE_ONE_CREATOR_NAME},
564 'created_at': NOTE_ONE_CREATED_AT
565 },
566 {
567 'id': 41381,
568 'body': NOTE_TWO_BODY,
569 'author': {'id': 1127, 'name': CREATOR_NAME},
570 'created_at': NOTE_TWO_CREATED_AT
571 }
572 ],
573 'https://gitlab.freedesktop.org/api/v4/projects/230/issues/':
574 [
575 {'id': 4675, 'iid': 3, 'project_id': 230},
576 {'id': 4674, 'iid': 2, 'project_id': 230},
577 {'id': 4673, 'iid': 1, 'project_id': 230}
578 ]
579 }
581 def __init__(self, url, **kwargs):
582 self.url = url
583 self.headers = {}
585 if url not in self.RESPONSES.keys():
586 raise ValueError("unknown URL: {}".format(url)) # pragma: no cover
588 if kwargs['headers']['PRIVATE-TOKEN'] != self.PRIVATE_TOKEN:
589 raise ValueError("GitLab needs PRIVATE-TOKEN for querying API") # pragma: no cover
591 RequestsGetMock.last_URL = url
592 RequestsGetMock.last_params = kwargs.get('params')
593 RequestsGetMock.last_headers = kwargs.get('headers')
595 def raise_for_status(self):
596 pass
598 def json(self):
599 return self.RESPONSES[self.url]
602class BugTrackerGitLabTests(TransactionTestCase):
603 def setUp(self):
604 url = "https://gitlab.freedesktop.org"
605 bug_base_url = "https://gitlab.freedesktop.org/patchwork-fdo/patchwork-fdo/issues/"
607 self.db_tracker = BugTracker.objects.create(tracker_type="gitlab", public=True,
608 project="230",
609 password=RequestsGetMock.PRIVATE_TOKEN,
610 url=url, bug_base_url=bug_base_url)
612 self.bug = Bug.objects.create(tracker=self.db_tracker, bug_id=str(RequestsGetMock.BUG_ID))
613 self.gitlab = GitLab(self.db_tracker)
615 @patch('requests.get')
616 def test_GetTrackerTime(self, req_mock):
617 resp_mock = MagicMock()
618 resp_mock.headers = {'Date': 'Thu, 07 May 2020 20:23:57 GMT'}
619 req_mock.return_value = resp_mock
620 dt = self.gitlab._get_tracker_time()
621 self.assertEqual(dt, datetime.datetime(2020, 5, 7, 20, 23, 57, tzinfo=pytz.utc))
623 def test_ToTrackerTz(self):
624 dt = timezone.now()
625 self.assertEqual(dt, self.gitlab._to_tracker_tz(dt))
627 @patch('requests.get', RequestsGetMock)
628 def testPolledBugShouldSaveJustFine(self):
629 self.gitlab._get_tracker_time = MagicMock(return_value=timezone.now())
630 self.gitlab.poll(self.bug)
631 self.bug.save()
633 @patch('requests.get', RequestsGetMock)
634 def testPollingBugShouldPopulateFields(self):
635 self.gitlab._get_tracker_time = MagicMock(return_value=timezone.now())
636 self.gitlab.poll(self.bug)
637 self.assertEqual(self.bug.title, RequestsGetMock.BUG_TITLE)
638 self.assertEqual(self.bug.status, RequestsGetMock.BUG_STATUS)
639 self.assertEqual(self.bug.assignee.person.full_name, RequestsGetMock.ASSIGNEE_NAME)
640 self.assertEqual(self.bug.creator.person.full_name, RequestsGetMock.CREATOR_NAME)
641 self.assertEqual(self.bug.created, dateparser.parse(RequestsGetMock.BUG_CREATED_AT))
642 self.assertEqual(self.bug.updated, dateparser.parse(RequestsGetMock.BUG_UPDATED_AT))
643 self.assertEqual(self.bug.description, RequestsGetMock.BUG_DESCRIPTION)
644 self.assertEqual(self.bug.product, RequestsGetMock.BUG_PRODUCT)
645 self.assertEqual(self.bug.component, RequestsGetMock.BUG_COMPONENT)
646 self.assertEqual(self.bug.priority, RequestsGetMock.BUG_PRIORITY)
647 self.assertEqual(self.bug.severity, RequestsGetMock.BUG_SEVERITY)
648 self.assertEqual(self.bug.platforms,
649 "{},{}".format(RequestsGetMock.BUG_PLATFORM1, RequestsGetMock.BUG_PLATFORM2))
650 self.assertEqual(self.bug.features,
651 "{},{}".format(RequestsGetMock.BUG_FEATURE1, RequestsGetMock.BUG_FEATURE2))
652 self.assertEqual(self.bug.tags, "FOO,BAR")
654 @patch('requests.get', RequestsGetMock)
655 def testPollingCustomFieldMap(self):
656 url = "https://gitlab.freedesktop.org"
657 bug_base_url = "https://gitlab.freedesktop.org/patchwork-fdo/patchwork-fdo/issues/"
658 custom_labels = ['target::'+RequestsGetMock.BUG_TARGET,
659 'mapped_severity::'+RequestsGetMock.BUG_MAP_SEVERITY]
661 db_tracker = BugTracker.objects.create(name='blah', tracker_type="gitlab", public=True,
662 project="230",
663 password=RequestsGetMock.PRIVATE_TOKEN,
664 url=url, bug_base_url=bug_base_url,
665 custom_fields_map={'target::': 'target',
666 'doesnt_exist': 'foo',
667 'mapped_severity::': 'severity'})
669 orig_labels = RequestsGetMock.BUG_LABELS
670 RequestsGetMock.BUG_LABELS.extend(custom_labels)
671 self.bug.tracker = db_tracker
672 self.bug.save()
673 gitlab = GitLab(db_tracker)
674 gitlab._get_tracker_time = MagicMock(return_value=timezone.now())
676 gitlab.poll(self.bug)
677 try:
678 self.assertEqual(self.bug.tags, "FOO,BAR") # make sure labels are still populated
679 self.assertEqual(self.bug.severity, RequestsGetMock.BUG_MAP_SEVERITY)
680 self.assertEqual(self.bug.custom_fields['target'], RequestsGetMock.BUG_TARGET)
681 self.assertIsNone(self.bug.custom_fields.get('foo'))
682 except: # pragma: no cover # noqa
683 raise
684 finally:
685 self.bug.tracker = self.db_tracker
686 self.bug.save()
687 RequestsGetMock.BUG_LABELS = orig_labels
689 @patch('requests.get', RequestsGetMock)
690 def testPollingBugShouldFetchComments(self):
691 self.gitlab._get_tracker_time = MagicMock(return_value=timezone.now())
692 self.gitlab.poll(self.bug)
694 comments = BugComment.objects.filter(bug=self.bug)
695 self.assertEqual(comments.count(), 2)
697 @patch('requests.get', RequestsGetMock)
698 def testNoteShouldBePopulatedCorrectly(self):
699 self.gitlab._get_tracker_time = MagicMock(return_value=timezone.now())
700 self.gitlab.poll(self.bug)
702 comment = BugComment.objects.get(bug=self.bug, comment_id=RequestsGetMock.NOTE_ONE_ID)
703 self.assertEqual(comment.account.person.full_name, RequestsGetMock.NOTE_ONE_CREATOR_NAME)
704 self.assertEqual(comment.created_on, dateparser.parse(RequestsGetMock.NOTE_ONE_CREATED_AT))
705 self.assertTrue("#note_{}".format(comment.comment_id) in comment.url)
706 self.assertTrue(self.db_tracker.bug_base_url in comment.url)
708 @patch('requests.get', RequestsGetMock)
709 def testPollingCreatesCommentList(self):
710 expected = [(RequestsGetMock.NOTE_ONE_CREATOR_NAME,
711 RequestsGetMock.NOTE_ONE_BODY,
712 dateparser.parse(RequestsGetMock.NOTE_ONE_CREATED_AT)),
713 (RequestsGetMock.CREATOR_NAME,
714 RequestsGetMock.NOTE_TWO_BODY,
715 dateparser.parse(RequestsGetMock.NOTE_TWO_CREATED_AT))]
717 comm_list = self.gitlab._GitLab__poll_comments(self.bug, "http://foo.com")
718 result = []
719 for c in comm_list:
720 result.append((c.db_object.account.person.full_name,
721 c.body,
722 c.db_object.created_on))
723 self.assertEqual(result, expected)
725 @patch('requests.get', RequestsGetMock)
726 def testPollingBugTwiceShouldNotDuplicateComments(self):
727 self.gitlab._get_tracker_time = MagicMock(return_value=timezone.now())
728 self.bug.comments_polled = dateparser.parse(RequestsGetMock.NOTE_ONE_CREATED_AT)
729 url = "{}#note_{}".format('https://gitlab.freedesktop.org/api/v4/projects/230/issues/2/notes',
730 RequestsGetMock.NOTE_ONE_ID)
732 person = Person.objects.create(full_name=RequestsGetMock.CREATOR_NAME)
733 account = BugTrackerAccount.objects.create(user_id="1", person=person,
734 tracker=self.db_tracker, is_developer=True)
735 BugComment.objects.create(bug=self.bug, account=account, comment_id=RequestsGetMock.NOTE_ONE_ID,
736 url=url,
737 created_on=dateparser.parse(RequestsGetMock.NOTE_ONE_CREATED_AT))
739 expected = [(RequestsGetMock.CREATOR_NAME,
740 RequestsGetMock.NOTE_TWO_BODY,
741 dateparser.parse(RequestsGetMock.NOTE_TWO_CREATED_AT))]
743 with patch('CIResults.bugtrackers.GitLab.check_replication') as cr_mock:
744 self.gitlab.poll(self.bug)
745 comm_list = cr_mock.call_args[0][1]
746 result = []
747 for c in comm_list:
748 result.append((c.db_object.account.person.full_name,
749 c.body,
750 c.db_object.created_on))
751 self.assertEqual(result, expected)
753 comments = BugComment.objects.filter(bug=self.bug)
754 self.assertEqual(comments.count(), 2)
756 self.gitlab.poll(self.bug)
757 comments = BugComment.objects.filter(bug=self.bug)
758 self.assertEqual(comments.count(), 2)
760 @patch('requests.get', RequestsGetMock)
761 def testSearchNoParams(self):
762 all_bugs = self.gitlab.search_bugs_ids()
763 self.assertEqual(all_bugs, set(['1', '2', '3']))
764 self.assertEqual(RequestsGetMock.last_params, {'page': 1, 'per_page': 100})
766 @patch('requests.get', RequestsGetMock)
767 def testSearchAllBugIds(self):
768 created_since = datetime.datetime.fromtimestamp(1000)
769 updated_since = datetime.datetime.fromtimestamp(1000)
770 all_bugs = self.gitlab.search_bugs_ids(created_since=created_since,
771 updated_since=updated_since,
772 components=['tag1', 'tag2'],
773 status='status1')
774 self.assertEqual(all_bugs, set(['1', '2', '3']))
775 self.assertEqual(RequestsGetMock.last_params, {
776 'page': 1, 'per_page': 100,
777 'created_after': created_since,
778 'updated_after': updated_since,
779 "labels": "tag1,tag2",
780 "state": 'status1'
781 })
783 @patch('requests.get', RequestsGetMock)
784 def testSearchWithOneStatusInList(self):
785 all_bugs = self.gitlab.search_bugs_ids(status=['status1'])
786 self.assertEqual(all_bugs, set(['1', '2', '3']))
787 self.assertEqual(RequestsGetMock.last_params, {
788 'page': 1, 'per_page': 100,
789 "state": 'status1'
790 })
792 def testSearchWithMoreThanOneStatus(self):
793 self.assertRaisesMessage(ValueError, "Status has to be a string",
794 self.gitlab.search_bugs_ids, status=['status1', 'status2'])
796 def test_open_statuses(self):
797 self.assertEqual(self.gitlab.open_statuses, ['opened'])
799 @patch('requests.post')
800 def testAddComment(self, post_mock):
801 comment = "Hello world!"
802 self.gitlab.add_comment(Bug(tracker=self.db_tracker, bug_id=RequestsGetMock.BUG_ID), comment)
804 # Check that the call was what was expected
805 args, kwargs = post_mock.call_args_list[0]
806 self.assertEqual(args[0], 'https://gitlab.freedesktop.org/api/v4/projects/230/issues/2/notes')
807 self.assertEqual(kwargs['headers'], {'PRIVATE-TOKEN': RequestsGetMock.PRIVATE_TOKEN})
808 self.assertEqual(kwargs['params'], {'body': comment})
810 @patch('requests.post')
811 def test_create_bug_from_json(self, post_mock):
812 summary = "summary"
813 description = "description"
814 test_id = 5678
815 json_bug = {'title': summary,
816 'description': description,
817 'labels': "Bug",
818 'state': "opened"}
820 post_mock.return_value.raise_for_status.return_value = None
821 post_mock.return_value.json.return_value = {"iid": test_id}
823 id = self.gitlab.create_bug_from_json(json_bug)
824 self.assertEqual(id, test_id)
826 args, kwargs = post_mock.call_args_list[0]
827 request = kwargs['params']
828 for field in json_bug:
829 self.assertEqual(request[field], json_bug[field])
831 @patch('requests.post')
832 def test_create_bug_from_json_no_labels(self, post_mock):
833 summary = "summary"
834 description = "description"
835 test_id = 5678
836 json_bug = {'title': summary,
837 'description': description}
839 post_mock.return_value.raise_for_status.return_value = None
840 post_mock.return_value.json.return_value = {"iid": test_id}
842 id = self.gitlab.create_bug_from_json(json_bug)
843 self.assertEqual(id, test_id)
845 args, kwargs = post_mock.call_args_list[0]
846 expected_request = {'title': summary,
847 'description': description}
848 request = kwargs['params']
849 for field in expected_request:
850 self.assertEqual(request[field], expected_request[field], field)
852 @patch('requests.post')
853 def test_create_bug_from_json_with_status(self, post_mock):
854 summary = "summary"
855 description = "description"
856 test_id = 5678
857 json_bug = {'title': summary,
858 'description': description,
859 'status': "opened"}
861 post_mock.return_value.raise_for_status.return_value = None
862 post_mock.return_value.json.return_value = {"iid": test_id}
864 id = self.gitlab.create_bug_from_json(json_bug)
865 self.assertEqual(id, test_id)
867 args, kwargs = post_mock.call_args_list[0]
868 expected_request = {'title': summary,
869 'description': description}
870 request = kwargs['params']
871 for field in expected_request:
872 self.assertEqual(request[field], expected_request[field], field)
874 @patch('requests.post')
875 def test_create_malformed_bug(self, post_mock):
876 summary = "summary"
877 description = "description"
878 json_bug = {'title': summary,
879 'description': description,
880 'labels': "Bug"}
882 post_mock.side_effect = requests.HTTPError
884 with self.assertRaises(ValueError):
885 self.gitlab.create_bug_from_json(json_bug)
887 @patch('requests.put')
888 def test_update_bug_from_json(self, put_mock):
889 json_bug = {'title': "summary",
890 'description': "description",
891 'labels': "Bug",
892 'state': "opened"}
894 self.gitlab.update_bug_from_json(json_bug, 5678)
896 args, kwargs = put_mock.call_args_list[0]
897 self.assertEqual(args[0], "https://gitlab.freedesktop.org/api/v4/projects/230/issues/5678")
898 for field in json_bug:
899 self.assertEqual(kwargs['params'][field], json_bug[field])
901 @patch('requests.put')
902 def test_update_bug_from_json_error(self, put_mock):
903 put_mock.side_effect = requests.HTTPError
904 with self.assertRaises(ValueError):
905 self.gitlab.update_bug_from_json({}, 5678)
907 def test_transition(self):
908 self.gitlab.update_bug_from_json = MagicMock()
909 self.gitlab.transition(1, "Open")
910 self.gitlab.update_bug_from_json.assert_called_with({'state_event': "Open"}, 1)
913class BugzillaProxyMock:
914 URL = "https://bugzilla.instance.org"
916 # User.login
917 LOGIN = "userlogin"
918 PASSWORD = "password"
919 TOKEN_ID = '12345'
920 TOKEN = '12345-kZ5CYMeQGH'
922 # Bug.add_comment
923 BUG_ID = 1234
924 BUG_ID_NO_EMAIL = 1235
925 BUG_ID_NON_EXISTING = 1236
926 BUG_ID_WRONG_COMMENT_COUNT = 1237
927 COMMENT = 'my comment'
929 # Bugzilla.create_bug
930 NEW_BUG_ID = 5678
931 PRODUCT = "TEST_PRODUCT"
932 COMPONENT = "TEST/COMPONENT/WITH/SLASHES"
933 SUMMARY = "TEST_SUMMARY"
934 DESCRIPTION = "TEST_DESCRIPTION"
935 DESCRIPTION_2 = "Some chump has run the data lines right through the power supply!"
937 # Update
938 UPDATE_IDS = 1
940 PROJECT = "{}/{}".format(PRODUCT, COMPONENT)
942 CREATE_REQUEST = {
943 'token': TOKEN,
944 'product': PRODUCT,
945 'component': COMPONENT,
946 'summary': SUMMARY,
947 'description': DESCRIPTION
948 }
949 # get_comments
950 COMMENT_CREATOR = "Roy Trenneman"
951 COMMENT_CREATOR_2 = "Maurice Moss"
952 COMMENT_CREATION_TIME = datetime.datetime.fromtimestamp(0)
953 COMMENT_2_CREATION_TIME = COMMENT_CREATION_TIME + timedelta(days=1)
955 class _Bugzilla:
956 def time(self):
957 return {'db_time': datetime.datetime.fromtimestamp(0)}
959 class _User:
960 def login(self, params):
961 if params.get('login') != BugzillaProxyMock.LOGIN:
962 raise ValueError('Incorrect or missing login') # pragma: no cover
963 if params.get('password') != BugzillaProxyMock.PASSWORD:
964 raise ValueError('Incorrect or missing password') # pragma: no cover
965 return {'id': BugzillaProxyMock.TOKEN_ID, 'token': BugzillaProxyMock.TOKEN}
967 class _Bug:
968 def get(self, params):
969 ids = params.get('ids')
970 if ids == BugzillaProxyMock.BUG_ID or ids == BugzillaProxyMock.BUG_ID_WRONG_COMMENT_COUNT:
971 creator_detail = {"real_name": "creator", "email": "creator@me.de"}
972 assigned_to_detail = {"real_name": "assignee", "email": "assignee@me.de"}
973 is_open = False
974 elif ids == BugzillaProxyMock.BUG_ID_NO_EMAIL:
975 creator_detail = {"real_name": "creator", "name": "creator"}
976 assigned_to_detail = {"real_name": "assignee", "name": "assignee"}
977 is_open = True
978 elif ids == BugzillaProxyMock.BUG_ID_NON_EXISTING:
979 return {'bugs': []}
980 else:
981 raise ValueError('Incorrect or missing bug id') # pragma: no cover
983 return {
984 "bugs": [{
985 "summary": "summary",
986 "status": "status",
987 "severity": "severity",
988 "is_open": is_open,
989 "resolution": "resolution",
990 "creation_time": datetime.datetime.fromtimestamp(0),
991 "last_change_time": datetime.datetime.fromtimestamp(5),
992 "creator_detail": creator_detail,
993 "assigned_to_detail": assigned_to_detail,
994 "product": "product",
995 "component": "component",
996 "custom_features": ["feature1", "feature2"],
997 "custom_platforms": ["platform1", "platform2"],
998 "priority": "high",
999 "a_custom_field": "I'm custom",
1000 "id": "invalid field",
1001 "bug_id": "invalid field",
1002 "tracker_id": "invalid field",
1003 "tracker": "invalid field",
1004 "parent_id": "invalid field",
1005 "parent": "invalid field"
1006 }]
1007 }
1009 def comments(self, params):
1010 if params.get('ids') == BugzillaProxyMock.BUG_ID or params.get('ids') == BugzillaProxyMock.BUG_ID_NO_EMAIL:
1011 count = 0
1012 elif params.get('ids') == BugzillaProxyMock.BUG_ID_WRONG_COMMENT_COUNT:
1013 count = 1
1014 else:
1015 raise ValueError('Incorrect or missing bug id') # pragma: no cover
1017 comments = {
1018 "comments": [
1019 {
1020 "text": BugzillaProxyMock.DESCRIPTION,
1021 "creator": BugzillaProxyMock.COMMENT_CREATOR,
1022 "id": 100,
1023 "count": count,
1024 "time": BugzillaProxyMock.COMMENT_CREATION_TIME,
1025 "creation_time": BugzillaProxyMock.COMMENT_CREATION_TIME
1026 },
1027 {
1028 "text": BugzillaProxyMock.DESCRIPTION_2,
1029 "creator": BugzillaProxyMock.COMMENT_CREATOR_2,
1030 "id": 101,
1031 "count": count+1,
1032 "time": BugzillaProxyMock.COMMENT_2_CREATION_TIME,
1033 "creation_time": BugzillaProxyMock.COMMENT_2_CREATION_TIME
1034 }
1035 ]
1036 }
1038 # prune the comments that came after the new_since parameter
1039 if params.get('new_since') is not None:
1040 new_since = params['new_since'].replace(tzinfo=None)
1041 comments['comments'] = [c for c in comments['comments'] if c['time'] > new_since]
1043 return {"bugs": {"1234": comments, "1235": comments, "1237": comments}}
1045 def history(self, params):
1046 if params.get('ids') != BugzillaProxyMock.BUG_ID:
1047 raise ValueError('Incorrect or missing bug id') # pragma: no cover
1049 return {"bugs": [
1050 {'history': [
1051 {'when': datetime.datetime.fromtimestamp(1), 'who': 'someone@toto.de',
1052 'changes': [{'field_name': 'status', 'removed': 'NEW', 'added': 'RESOLVED'},
1053 {'field_name': 'resolution', 'removed': '', 'added': 'FIXED'}]
1054 },
1055 {'who': 'someone@toto.de', 'when': datetime.datetime.fromtimestamp(2),
1056 'changes': [
1057 {'field_name': 'status', 'added': 'NEW', 'removed': 'RESOLVED'},
1058 {'field_name': 'resolution', 'added': '', 'removed': 'FIXED'}],
1059 },
1060 {'when': datetime.datetime.fromtimestamp(3), 'who': 'someone@toto.de',
1061 'changes': [
1062 {'field_name': 'status', 'added': 'RESOLVED', 'removed': 'NEW'},
1063 {'field_name': 'resolution', 'removed': '', 'added': 'FIXED'}],
1064 },
1065 {'when': datetime.datetime.fromtimestamp(4), 'who': 'someone@toto.de',
1066 'changes': [{'field_name': 'status', 'added': 'CLOSED', 'removed': 'RESOLVED'}],
1067 }]
1068 }] # noqa
1069 }
1071 def add_comment(self, params):
1072 if params.get('id') != BugzillaProxyMock.BUG_ID:
1073 raise ValueError('Incorrect or missing bug id') # pragma: no cover
1074 if params.get('token') != BugzillaProxyMock.TOKEN:
1075 raise ValueError('Incorrect or missing token') # pragma: no cover
1076 if params.get('comment') != BugzillaProxyMock.COMMENT:
1077 raise ValueError('Incorrect or missing comment') # pragma: no cover
1078 return {'id': 766846}
1080 def create(self, params):
1081 if params.get('token') != BugzillaProxyMock.TOKEN:
1082 raise xmlrpc.client.Error('Incorrect or missing token') # pragma: no cover
1083 if params.get('summary') != BugzillaProxyMock.SUMMARY:
1084 raise xmlrpc.client.Error('Incorrect or missing summary') # pragma: no cover
1085 if params.get('description') != BugzillaProxyMock.DESCRIPTION:
1086 raise xmlrpc.client.Error('Incorrect or missing description') # pragma: no cover
1088 return {'id': '1'}
1090 def search(self, params):
1091 self.last_search_request = params
1092 return {'bugs': [{"id": 10}, {"id": 11}, {"id": 13}]}
1094 def update(self, params):
1095 if params.get('ids') != BugzillaProxyMock.UPDATE_IDS:
1096 raise xmlrpc.client.Error('Incorrect or missing ids') # pragma: no cover
1098 # All the other checks are the same as create(), so just call that
1099 self.create(params)
1101 def __init__(self, url, use_builtin_types=False):
1102 if url != self.URL + "/xmlrpc.cgi":
1103 raise ValueError('invalid xmlrpc url') # pragma: no cover
1105 if not use_builtin_types:
1106 raise ValueError('use_builtin_types is not True') # pragma: no cover
1108 User = _User()
1109 Bug = _Bug()
1110 Bugzilla = _Bugzilla()
1113class BugTrackerBugzillaTests(TestCase):
1114 @patch('xmlrpc.client.ServerProxy', BugzillaProxyMock)
1115 def setUp(self):
1116 self.tracker = BugTracker.objects.create(tracker_type="bugzilla", public=True,
1117 url=BugzillaProxyMock.URL,
1118 username=BugzillaProxyMock.LOGIN,
1119 password=BugzillaProxyMock.PASSWORD,
1120 custom_fields_map={'custom_features': 'features',
1121 'custom_platforms': 'platforms',
1122 'a_custom_field': 'my_custom_field'},
1123 name='my tracker')
1124 self.bugzilla = Bugzilla(self.tracker)
1126 def test__get_tracker_time(self):
1127 dt = datetime.datetime.fromtimestamp(0, pytz.utc)
1128 self.assertEqual(dt, self.bugzilla._get_tracker_time())
1130 def test__to_tracker_tz(self):
1131 dt = datetime.datetime.fromtimestamp(0, pytz.utc)
1132 dt_pdt = dt.astimezone(pytz.timezone('America/Vancouver'))
1133 self.assertEqual(dt, self.bugzilla._to_tracker_tz(dt_pdt))
1135 def test__get_user_id(self):
1136 self.assertEqual(Bugzilla._get_user_id({'creator_detail': {'email': 'me@email.com',
1137 'name': 'John Doe'}},
1138 'creator'), 'me@email.com')
1140 self.assertEqual(Bugzilla._get_user_id({'creator_detail': {'name': 'John Doe'}},
1141 'creator'), 'John Doe')
1143 self.assertRaisesMessage(ValueError,
1144 'Cannot find a good identifier for the user of the bug 1234',
1145 Bugzilla._get_user_id, {'id': '1234'}, 'creator')
1147 def test_list_to_str(self):
1148 self.assertEqual(Bugzilla._list_to_str(['one', 'two', 'three']), 'one,two,three')
1149 self.assertEqual(Bugzilla._list_to_str('one'), 'one')
1151 def test_bug_id_parser(self):
1152 self.assertEqual(Bugzilla._bug_id_parser(MagicMock(spec=Bug, bug_id='1234')), 1234)
1153 self.assertRaisesMessage(ValueError, "Bugzilla's IDs should be integers (fdo#1234)",
1154 Bugzilla._bug_id_parser, MagicMock(spec=Bug, bug_id='fdo#1234'))
1156 def test__parse_custom_field(self):
1157 value1 = ['apple', 'cherry', 'orange']
1158 resp1 = self.bugzilla._parse_custom_field(value1, to_str=False)
1159 resp1_str = self.bugzilla._parse_custom_field(value1, to_str=True)
1160 self.assertEqual(resp1, value1)
1161 self.assertEqual(resp1_str, 'apple,cherry,orange')
1163 value2 = 'something clever'
1164 resp2 = self.bugzilla._parse_custom_field(value2, to_str=False)
1165 resp2_str = self.bugzilla._parse_custom_field(value2, to_str=True)
1166 self.assertEqual(resp2, value2)
1167 self.assertEqual(resp2_str, value2)
1169 def test_poll__with_emails(self):
1170 bug = Bug.objects.create(tracker=self.tracker, bug_id=BugzillaProxyMock.BUG_ID, closed=None, description=None)
1171 bug.save = MagicMock()
1173 with patch('CIResults.bugtrackers.Bugzilla.check_replication') as cr_mock:
1174 self.bugzilla.poll(bug)
1175 comm_list = cr_mock.call_args[0][1]
1177 result = []
1178 for c in comm_list:
1179 result.append((c.db_object.account.person.email,
1180 c.body,
1181 c.db_object.created_on))
1183 self.assertEqual(bug.title, "summary")
1184 self.assertEqual(bug.created, datetime.datetime.fromtimestamp(0, tz=pytz.utc))
1185 self.assertEqual(bug.updated, datetime.datetime.fromtimestamp(5, tz=pytz.utc))
1186 self.assertEqual(bug.closed, datetime.datetime.fromtimestamp(3, tz=pytz.utc))
1187 self.assertEqual(bug.creator.person.full_name, "creator")
1188 self.assertEqual(bug.creator.person.email, "creator@me.de")
1189 self.assertEqual(bug.assignee.person.full_name, "assignee")
1190 self.assertEqual(bug.assignee.person.email, "assignee@me.de")
1191 self.assertEqual(bug.product, "product")
1192 self.assertEqual(bug.component, "component")
1193 self.assertEqual(bug.features, "feature1,feature2")
1194 self.assertEqual(bug.platforms, "platform1,platform2")
1195 self.assertEqual(bug.status, "status/resolution")
1196 self.assertEqual(bug.priority, "high")
1197 self.assertEqual(bug.severity, "severity")
1198 self.assertEqual(bug.description, BugzillaProxyMock.DESCRIPTION)
1199 self.assertEqual(bug.custom_fields['my_custom_field'], "I'm custom")
1200 bug.save.assert_not_called()
1202 expected = [(BugzillaProxyMock.COMMENT_CREATOR,
1203 BugzillaProxyMock.DESCRIPTION,
1204 timezone.make_aware(BugzillaProxyMock.COMMENT_CREATION_TIME, pytz.utc)),
1205 (BugzillaProxyMock.COMMENT_CREATOR_2,
1206 BugzillaProxyMock.DESCRIPTION_2,
1207 timezone.make_aware(BugzillaProxyMock.COMMENT_2_CREATION_TIME, pytz.utc))]
1208 # Check that we polled all the new comments
1209 self.assertEqual(result, expected)
1211 @patch('xmlrpc.client.ServerProxy', BugzillaProxyMock)
1212 def test_poll_invalid_custom_fields(self):
1213 tracker = BugTracker.objects.create(tracker_type="bugzilla", public=True,
1214 url=BugzillaProxyMock.URL,
1215 username=BugzillaProxyMock.LOGIN,
1216 password=BugzillaProxyMock.PASSWORD,
1217 custom_fields_map={'id': 'id',
1218 'bug_id': 'bug_id',
1219 'tracker_id': 'tracker_id',
1220 'tracker': 'tracker',
1221 'parent_id': 'parent_id',
1222 'parent': 'parent'},
1223 name='super tracker')
1224 bugzilla = Bugzilla(tracker)
1225 bug = Bug.objects.create(tracker=tracker, bug_id=BugzillaProxyMock.BUG_ID, closed=None, description=None)
1226 bug.save = MagicMock()
1227 parse_mock = MagicMock()
1228 bugzilla._parse_custom_field = parse_mock
1230 with patch('CIResults.bugtrackers.Bugzilla.check_replication'):
1231 bugzilla.poll(bug)
1233 parse_mock.assert_not_called()
1235 def test_poll__new_comments_arrived(self):
1236 bug = Bug.objects.create(tracker=self.tracker, bug_id=BugzillaProxyMock.BUG_ID, closed=None, description=None,
1237 comments_polled=timezone.make_aware(BugzillaProxyMock.COMMENT_CREATION_TIME, pytz.utc))
1239 comm_list = self.bugzilla._Bugzilla__poll_comments(bug)
1240 result = []
1241 for c in comm_list:
1242 result.append((c.db_object.account.person.email,
1243 c.body,
1244 c.db_object.created_on))
1246 expected = [(BugzillaProxyMock.COMMENT_CREATOR_2,
1247 BugzillaProxyMock.DESCRIPTION_2,
1248 timezone.make_aware(BugzillaProxyMock.COMMENT_2_CREATION_TIME, pytz.utc))]
1249 # Check that we polled only the new comment
1250 self.assertEqual(result, expected)
1252 def test_poll__no_emails(self):
1253 bug = MagicMock(spec=Bug, bug_id=BugzillaProxyMock.BUG_ID_NO_EMAIL, comments_polled=None)
1254 with patch.object(BugComment.objects, "create"):
1255 self.bugzilla.poll(bug)
1257 self.assertEqual(bug.closed, None)
1258 self.assertEqual(bug.creator.person.full_name, "creator")
1259 self.assertEqual(bug.creator.person.email, None)
1260 self.assertEqual(bug.assignee.person.full_name, "assignee")
1261 self.assertEqual(bug.assignee.person.email, None)
1262 bug.save.assert_not_called()
1264 def test_poll_invalid_bug(self):
1265 bug = MagicMock(spec=Bug, bug_id=BugzillaProxyMock.BUG_ID_NON_EXISTING, tracker=self.tracker)
1266 self.assertRaisesMessage(ValueError, "Could not find the bug ID 1236 on my tracker",
1267 self.bugzilla.poll, bug)
1269 bug.save.assert_not_called()
1271 def test_poll_wrong_comment_count(self):
1272 bug = MagicMock(spec=Bug, bug_id=BugzillaProxyMock.BUG_ID_WRONG_COMMENT_COUNT, description=None)
1273 with patch.object(BugComment.objects, "create"):
1274 with self.assertRaises(ValueError):
1275 self.bugzilla.poll(bug)
1276 bug.save.assert_not_called()
1278 def test_search_bugs_ids__full(self):
1279 # Get the list of open bugs
1280 updated_since = datetime.datetime.fromtimestamp(1000)
1281 open_bugs = self.bugzilla.search_bugs_ids(components=["COMPONENT1", "COMPONENT2"],
1282 created_since=datetime.datetime.fromtimestamp(1000),
1283 updated_since=updated_since,
1284 status=['status1', 'status2'])
1285 self.assertEqual(open_bugs, set(['10', '11', '13']))
1287 # Verify that the request was valid
1288 expected_request = {
1289 "component": ["COMPONENT1", "COMPONENT2"],
1290 "status": ['status1', 'status2'],
1291 "creation_time": datetime.datetime.fromtimestamp(1000),
1292 "last_change_time": updated_since,
1293 "include_fields": ['id']
1294 }
1295 self.assertEqual(BugzillaProxyMock.Bug.last_search_request, expected_request)
1297 def test_search_bugs_ids__empty(self):
1298 self.bugzilla.search_bugs_ids()
1299 expected_request = {
1300 "include_fields": ['id']
1301 }
1302 self.assertEqual(BugzillaProxyMock.Bug.last_search_request, expected_request)
1304 def test_open_statuses(self):
1305 self.assertEqual(self.bugzilla.open_statuses, ["NEW", "ASSIGNED", "REOPENED", "NEEDINFO"])
1307 def test_auth_login(self):
1308 self.assertEqual(self.bugzilla.get_auth_token(), BugzillaProxyMock.TOKEN)
1310 def test_auth_login__invalid_username(self):
1311 for username in [None, ""]:
1312 self.tracker.username = username
1313 self.assertRaisesMessage(ValueError, "Invalid credentials",
1314 self.bugzilla.get_auth_token)
1316 def test_auth_login__invalid_password(self):
1317 for password in [None, ""]:
1318 self.tracker.password = password
1319 self.assertRaisesMessage(ValueError, "Invalid credentials",
1320 self.bugzilla.get_auth_token)
1322 def test_add_comment(self):
1323 bug = Bug(tracker=self.tracker, bug_id=str(BugzillaProxyMock.BUG_ID))
1324 self.bugzilla.add_comment(bug, BugzillaProxyMock.COMMENT)
1326 @patch('CIResults.bugtrackers.Bugzilla.get_auth_token', return_value=None)
1327 def test_add_comment__invalid_credentials(self, auth_token_mocked):
1328 bug = Bug(tracker=self.tracker, bug_id=str(BugzillaProxyMock.BUG_ID))
1329 self.assertRaisesMessage(ValueError, "Authentication failed. Can't post a comment",
1330 self.bugzilla.add_comment, bug, BugzillaProxyMock.COMMENT)
1332 @patch('CIResults.bugtrackers.Bugzilla.get_auth_token', return_value=BugzillaProxyMock.TOKEN)
1333 def test_create_bug_from_json(self, auth_mock):
1334 json_bug = {'summary': BugzillaProxyMock.SUMMARY,
1335 'description': BugzillaProxyMock.DESCRIPTION}
1336 self.bugzilla.create_bug_from_json(json_bug)
1338 @patch('CIResults.bugtrackers.Bugzilla.get_auth_token', return_value=BugzillaProxyMock.TOKEN)
1339 def test_create_bug_from_json__with_title_insteaf_of_summary(self, auth_mock):
1340 json_bug = {'title': BugzillaProxyMock.SUMMARY,
1341 'description': BugzillaProxyMock.DESCRIPTION}
1342 self.bugzilla.create_bug_from_json(json_bug)
1344 @patch('CIResults.bugtrackers.Bugzilla.get_auth_token', return_value=BugzillaProxyMock.TOKEN)
1345 def test_create_bug_from_json__no_summary_nor_title(self, auth_mock):
1346 with self.assertRaises(KeyError):
1347 self.bugzilla.create_bug_from_json({})
1349 @patch('CIResults.bugtrackers.Bugzilla.get_auth_token', return_value=BugzillaProxyMock.TOKEN)
1350 def test_create_bug_from_json__missing_description(self, auth_mock):
1351 json_bug = {'title': BugzillaProxyMock.SUMMARY}
1352 with self.assertRaises(ValueError):
1353 self.bugzilla.create_bug_from_json(json_bug)
1355 @patch('CIResults.bugtrackers.Bugzilla.get_auth_token', return_value=None)
1356 def test_create_bug_from_json_invalid_token(self, auth_mock):
1357 with self.assertRaises(ValueError):
1358 self.bugzilla.create_bug_from_json({})
1360 @patch('CIResults.bugtrackers.Bugzilla.get_auth_token', return_value=None)
1361 def test_create_bug_from_json_missing_required(self, auth_mock):
1362 with self.assertRaises(ValueError):
1363 self.bugzilla.create_bug_from_json({'summary': BugzillaProxyMock.SUMMARY})
1365 @patch('CIResults.bugtrackers.Bugzilla.get_auth_token', return_value=BugzillaProxyMock.TOKEN)
1366 def test_update_bug_from_json(self, auth_mock):
1367 json_bug = {'summary': BugzillaProxyMock.SUMMARY,
1368 'description': BugzillaProxyMock.DESCRIPTION}
1369 self.bugzilla.update_bug_from_json(json_bug, BugzillaProxyMock.UPDATE_IDS)
1371 @patch('CIResults.bugtrackers.Bugzilla.get_auth_token', return_value=BugzillaProxyMock.TOKEN)
1372 def test_update_bug_from_json_error(self, auth_mock):
1373 self.bugzilla._proxy.Bug.update = MagicMock(side_effect=xmlrpc.client.Error())
1374 with self.assertRaises(ValueError):
1375 self.bugzilla.update_bug_from_json({}, 1)
1377 @patch('CIResults.bugtrackers.Bugzilla.get_auth_token', return_value=None)
1378 def test_update_bug_from_json_invalid_token(self, auth_mock):
1379 with self.assertRaises(ValueError):
1380 self.bugzilla.update_bug_from_json({}, 1)
1382 def test_transition(self):
1383 self.bugzilla.update_bug_from_json = MagicMock()
1384 self.bugzilla.transition(1, "Open")
1385 self.bugzilla.update_bug_from_json.assert_called_with({'status': "Open"}, 1)
1388class JiraMock:
1389 # New Bug
1390 NEW_BUG_ID = 5678
1391 PROJECT_KEY = "TEST"
1392 ISSUE_KEY = "TEST-101"
1393 SUMMARY = "This is a test bug"
1394 DESCRIPTION = "This is a description"
1396 ISSUE = MagicMock()
1397 ISSUE.key = ISSUE_KEY
1399 # URLs
1400 URL = "https://jira.instance.com/rest/api/2/issue"
1401 RESP_URL = urllib.parse.urljoin(URL, str(NEW_BUG_ID))
1402 REQ_URL = URL
1404 # User.login
1405 LOGIN = "userlogin"
1406 PASSWORD = "password"
1408 # Bug.add_comment
1409 BUG_ID = 1234
1410 COMMENT = 'my comment'
1412 # Bug.create_bug
1413 REQUEST_DATA = {"project": {
1414 "key": PROJECT_KEY
1415 },
1416 "summary": SUMMARY,
1417 "description": DESCRIPTION,
1418 "issuetype": {
1419 "name": "Bug"
1420 }
1421 }
1423 RESPONSES = {REQ_URL:
1424 {"id": NEW_BUG_ID,
1425 "key": ISSUE_KEY,
1426 "self": RESP_URL}}
1429class BugTrackerJiraTests(TransactionTestCase):
1430 def setUp(self):
1431 issue = MagicMock(fields=MagicMock(summary="summary", status=MagicMock(),
1432 description="description",
1433 created=datetime.datetime.fromtimestamp(0, tz=pytz.utc).isoformat(),
1434 updated=datetime.datetime.fromtimestamp(1, tz=pytz.utc).isoformat(),
1435 resolutiondate=datetime.datetime.fromtimestamp(42, tz=pytz.utc).isoformat(),
1436 creator=MagicMock(displayName="creator", key="creator_key",
1437 emailAddress="creator@email"),
1438 assignee=MagicMock(displayName="assignee", key="assignee_key",
1439 emailAddress="assigne@email"),
1440 components=[MagicMock(), MagicMock()],
1441 comment=MagicMock(comments=[])))
1442 type(issue.fields.status).name = PropertyMock(return_value='status')
1443 type(issue.fields.priority).name = PropertyMock(return_value='Low')
1444 type(issue.fields.components[0]).name = PropertyMock(return_value='component1')
1445 type(issue.fields.components[1]).name = PropertyMock(return_value='component2')
1446 feature1 = MagicMock(value='Lasers')
1447 feature2 = MagicMock(value='Sharks')
1448 issue.fields.cool_features = [feature1, feature2]
1449 platform1 = MagicMock(value='Platform A')
1450 platform2 = MagicMock(value='Platform B')
1451 issue.fields.fancy_platforms = [platform1, platform2]
1452 issue.fields.a_custom_field = MagicMock(value="tacos")
1453 issue.fields.labels = ['label1', 'label2']
1455 comment_created = datetime.datetime.fromtimestamp(47, tz=pytz.utc).isoformat()
1456 issue.fields.comment.comments.append(MagicMock(author=MagicMock(displayName='Last, First',
1457 emailAddress='comments@email'),
1458 id='12345', created=comment_created))
1459 type(issue.fields.comment.comments[0].author).name = PropertyMock(return_value='flast')
1460 issue.fields.comment.comments.append(MagicMock(author=MagicMock(displayName='Last, First2',
1461 emailAddress='comments@email2'),
1462 id='12346', created=comment_created,))
1463 type(issue.fields.comment.comments[1].author).name = PropertyMock(return_value='flast2')
1465 self.issue = issue
1467 @patch('CIResults.bugtrackers.Jira.jira')
1468 def test__get_tracker_time(self, j_mock):
1469 tracker = BugTracker.objects.create(tracker_type="jira", project='PRODUCT', public=True)
1470 jira = Jira(tracker)
1472 j_mock.server_info.return_value = {'serverTime': '2020-05-07T10:36:28.772-0700'}
1473 dt = datetime.datetime(2020, 5, 7, 17, 36, 28, 772000, tzinfo=pytz.utc)
1474 jt = jira._get_tracker_time()
1476 self.assertEqual(dt, jt)
1478 @patch('CIResults.bugtrackers.timezone')
1479 @patch('CIResults.bugtrackers.Jira.jira')
1480 def test__get_tracker_time_error(self, j_mock, tz_mock):
1481 tracker = BugTracker.objects.create(tracker_type="jira", project='PRODUCT', public=True)
1482 jira = Jira(tracker)
1484 j_mock.server_info = MagicMock(side_effect=JIRAError)
1485 now_mock = MagicMock()
1486 tz_mock.now = now_mock
1487 jira._get_tracker_time()
1489 now_mock.assert_called()
1491 @patch('CIResults.bugtrackers.Jira.jira')
1492 def test__to_tracker_tz(self, j_mock):
1493 tracker = BugTracker.objects.create(tracker_type="jira", project='PRODUCT', public=True)
1494 jira = Jira(tracker)
1496 j_mock.myself.return_value = {'timeZone': 'America/Vancouver'}
1497 dt = datetime.datetime(2020, 5, 7, 17, 36, 28, 772000, tzinfo=pytz.utc)
1498 j_dt = jira._to_tracker_tz(dt)
1500 # NOTE: datetime comparison considers TZ, so format to str to
1501 # make sure right datetime adjusted for right TZ.
1502 exp_str = "2020/05/07 10:36"
1503 jt_str = j_dt.strftime("%Y/%m/%d %H:%M")
1505 self.assertEqual(exp_str, jt_str)
1507 @patch('CIResults.bugtrackers.Jira.jira')
1508 def test__to_tracker_tz_error(self, j_mock):
1509 tracker = BugTracker.objects.create(tracker_type="jira", project='PRODUCT', public=True)
1510 jira = Jira(tracker)
1512 j_mock.myself = MagicMock(side_effect=JIRAError)
1513 dt = datetime.datetime(2020, 5, 7, 17, 36, 28, 772000, tzinfo=pytz.utc)
1514 j_dt = jira._to_tracker_tz(dt)
1516 # NOTE: datetime comparison considers TZ, so format to str to
1517 # make sure right datetime adjusted for right TZ.
1518 exp_str = "2020/05/07 17:36"
1519 jt_str = j_dt.strftime("%Y/%m/%d %H:%M")
1521 self.assertEqual(exp_str, jt_str)
1523 @patch('jira.JIRA.__init__', return_value=None)
1524 def test_jira__no_auth(self, JIRA_mocked):
1525 Jira(BugTracker(tracker_type="jira", url='https://jira.com', public=True)).jira
1526 JIRA_mocked.assert_called_with({'server': 'https://jira.com', 'verify': False})
1528 @patch('jira.JIRA.__init__', return_value=None)
1529 def test_jira__with_auth(self, JIRA_mocked):
1530 Jira(BugTracker(tracker_type="jira", url='https://jira.com', public=True,
1531 username='user', password='password')).jira
1532 JIRA_mocked.assert_called_with({'server': 'https://jira.com', 'verify': False},
1533 basic_auth=('user', 'password'))
1535 def test__parse_custom_field(self):
1536 tracker = BugTracker.objects.create(tracker_type="jira", project='PRODUCT',
1537 public=True)
1538 jira = Jira(tracker)
1540 value1 = ['apple', 'cherry', 'orange']
1541 resp1 = jira._parse_custom_field(value1, to_str=False)
1542 resp1_str = jira._parse_custom_field(value1, to_str=True)
1543 self.assertEqual(resp1, value1)
1544 self.assertEqual(resp1_str, 'apple,cherry,orange')
1546 value2 = 'something clever'
1547 resp2 = jira._parse_custom_field(value2, to_str=False)
1548 resp2_str = jira._parse_custom_field(value2, to_str=True)
1549 self.assertEqual(resp2, value2)
1550 self.assertEqual(resp2_str, value2)
1552 value3 = [MagicMock(value="apple"), MagicMock(value="cherry"), MagicMock(value="orange")]
1553 resp3 = jira._parse_custom_field(value3, to_str=False)
1554 resp3_str = jira._parse_custom_field(value3, to_str=True)
1555 self.assertEqual(resp3, ['apple', 'cherry', 'orange'])
1556 self.assertEqual(resp3_str, 'apple,cherry,orange')
1558 value4 = MagicMock(value=25)
1559 resp4 = jira._parse_custom_field(value4, to_str=False)
1560 resp4_str = jira._parse_custom_field(value4, to_str=True)
1561 self.assertEqual(resp4, value4.value)
1562 self.assertEqual(resp4_str, "25")
1564 @patch('CIResults.bugtrackers.Jira.jira')
1565 def test_poll(self, connection_mock):
1566 connection_mock.issue.return_value = self.issue
1567 connection_mock.server_info.return_value = {'serverTime': '2020-05-07T10:36:28.772-0700'}
1569 tracker = BugTracker.objects.create(tracker_type="jira", bug_base_url='https://jira.com/browse/',
1570 public=True, custom_fields_map={'cool_features': 'features',
1571 'fancy_platforms': 'platforms',
1572 'a_custom_field': 'my_custom_field'})
1573 bug = Bug(bug_id='1234', tracker=tracker)
1574 bug._save = bug.save
1575 bug.save = MagicMock()
1576 Jira(tracker).poll(bug)
1578 fields = connection_mock.issue.call_args[1]['fields']
1579 self.assertIn('fancy_platforms', fields)
1580 self.assertIn('cool_features', fields)
1581 self.assertEqual(bug.title, "summary")
1582 self.assertEqual(bug.created, datetime.datetime.fromtimestamp(0, tz=pytz.utc))
1583 self.assertEqual(bug.updated, datetime.datetime.fromtimestamp(1, tz=pytz.utc))
1584 self.assertEqual(bug.creator.person.full_name, "creator")
1585 self.assertEqual(bug.assignee.person.full_name, "assignee")
1586 self.assertEqual(bug.component, "component1,component2")
1587 self.assertEqual(bug.status, "status")
1588 self.assertEqual(bug.priority, "Low")
1589 self.assertEqual(bug.platforms, "Platform A,Platform B")
1590 self.assertEqual(bug.features, "Lasers,Sharks")
1591 self.assertEqual(bug.description, "description")
1592 self.assertEqual(bug.closed, datetime.datetime.fromtimestamp(42, tz=pytz.utc))
1593 self.assertEqual(bug.custom_fields['my_custom_field'], "tacos")
1594 self.assertEqual(bug.tags, "label1,label2")
1596 del (tracker.custom_fields_map['cool_features'])
1597 del (tracker.custom_fields_map['fancy_platforms'])
1598 Jira(tracker).poll(bug)
1599 self.assertIsNone(bug.platforms)
1600 self.assertIsNone(bug.features)
1602 # Check that the bug does not exist and no bugs have been polled
1603 bug.save.assert_not_called()
1604 self.assertEqual(len(BugComment.objects.all()), 0)
1606 # Save the bug, then poll again to check that the comments get created
1607 bug._save()
1608 bug.poll()
1609 bug.save.assert_not_called()
1611 # Check that the comments got created
1612 for c_id in ['12345', '12346']:
1613 comment = BugComment.objects.get(comment_id=c_id)
1614 self.assertEqual(comment.bug, bug)
1615 self.assertIn("flast", comment.account.user_id)
1616 self.assertIn("Last, First", comment.account.person.full_name)
1617 self.assertEqual(comment.url, "https://jira.com/browse/1234#comment-{}".format(c_id))
1618 self.assertEqual(comment.created_on, datetime.datetime.fromtimestamp(47, tz=pytz.utc))
1620 @patch('CIResults.bugtrackers.Jira.jira')
1621 def test_poll_invalid_custom_fields(self, connection_mock):
1622 tracker = BugTracker.objects.create(tracker_type="jira", bug_base_url='https://jira.com/browse/',
1623 public=True, custom_fields_map={'id': 'id',
1624 'bug_id': 'bug_id',
1625 'tracker_id': 'tracker_id',
1626 'tracker': 'tracker',
1627 'parent_id': 'parent_id',
1628 'parent': 'parent'})
1629 bug = Bug(bug_id='1234', tracker=tracker)
1630 bug._save = bug.save
1631 bug.save = MagicMock()
1633 self.issue.fields.id = 9000
1634 self.issue.fields.bug_id = 4000
1635 self.issue.fields.tracker_id = 12341234
1636 self.issue.fields.parent_id = 9999999
1637 self.issue.fields.parent = 102030102
1638 connection_mock.issue.return_value = self.issue
1640 j = Jira(tracker)
1641 parse_mock = MagicMock()
1642 j._parse_custom_field = parse_mock
1643 j.poll(bug)
1645 parse_mock.assert_not_called()
1647 @patch('CIResults.bugtrackers.Jira.jira')
1648 def test_poll_invalid_status(self, connection_mock):
1649 tracker = BugTracker.objects.create(tracker_type="jira", bug_base_url='https://jira.com/browse/',
1650 public=True)
1651 j = Jira(tracker)
1653 bug = Bug(bug_id='1234', tracker=tracker)
1654 bug._save = bug.save
1655 bug.save = MagicMock()
1657 issues = []
1658 for i in range(3):
1659 issues.append(copy.deepcopy(self.issue))
1661 type(issues[0].fields.status).name = PropertyMock(return_value='New State')
1662 type(issues[1].fields.status).name = PropertyMock(return_value='New State')
1663 type(issues[2].fields.status).name = PropertyMock(return_value='Open')
1665 connection_mock.issue.side_effect = issues
1667 j.poll(bug)
1668 self.assertEqual(bug.status, "Open")
1670 type(issues[2].fields.status).name = PropertyMock(return_value='New State')
1672 connection_mock.issue.side_effect = issues
1674 j.poll(bug)
1675 self.assertEqual(bug.status, "New State")
1677 def create_issue(self, comm_created, comm2_created):
1678 issue = MagicMock(fields=MagicMock(comment=MagicMock(comments=[])))
1680 name_1 = 'Pat'
1681 name_2 = 'Geno'
1682 email_1 = 'Pat@Pat'
1683 email_2 = 'Geno@Geno'
1684 comm_body_1 = "Steak wiz wit'"
1685 comm_body_2 = "Steak prov witout"
1686 issue.fields.comment.comments.append(MagicMock(author=MagicMock(displayName=name_1, emailAddress=email_1),
1687 id='12345', created=str(comm_created), body=comm_body_1))
1688 type(issue.fields.comment.comments[0].author).name = PropertyMock(return_value=name_1)
1689 issue.fields.comment.comments.append(MagicMock(author=MagicMock(displayName=name_2, emailAddress=email_2),
1690 id='12346', created=str(comm2_created), body=comm_body_2))
1691 type(issue.fields.comment.comments[1].author).name = PropertyMock(return_value=name_2)
1693 JiraMock.issue = MagicMock()
1694 JiraMock.issue.return_value = issue
1696 comments = [(name_1,
1697 comm_body_1,
1698 comm_created),
1699 (name_2,
1700 comm_body_2,
1701 comm2_created)]
1702 return (issue, comments)
1704 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
1705 def test__poll_comments(self):
1706 comment_created = timezone.make_aware(datetime.datetime.now(), pytz.utc)
1707 comment2_created = timezone.make_aware(datetime.datetime.now(), pytz.utc)
1708 issue, exp_resp = self.create_issue(comment_created, comment2_created)
1710 tracker = BugTracker.objects.create(tracker_type="jira", bug_base_url='https://jira.com/browse/',
1711 public=True)
1712 bug = Bug(bug_id='1234', tracker=tracker)
1713 bug.save()
1715 j = Jira(tracker)
1716 comm_list = j._Jira__poll_comments(bug, issue)
1717 result = []
1718 for c in comm_list:
1719 result.append((c.db_object.account.person.full_name,
1720 c.body,
1721 c.db_object.created_on))
1723 self.assertEqual(result, exp_resp)
1725 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
1726 def test__poll_comments_after_polled(self):
1727 comment_created = timezone.make_aware(datetime.datetime.now(), pytz.utc)
1728 comment2_created = comment_created + timedelta(days=2)
1729 comment_polled = comment_created + timedelta(days=1)
1730 tracker = BugTracker.objects.create(tracker_type="jira", bug_base_url='https://jira.com/browse/',
1731 public=True)
1732 j = Jira(tracker)
1733 bug = Bug(bug_id='1234', tracker=tracker, comments_polled=comment_polled)
1734 bug.save()
1736 issue, exp_resp = self.create_issue(comment_created, comment2_created)
1737 exp_resp = [exp_resp[1]]
1738 person = Person.objects.create(full_name=exp_resp[0])
1739 account = BugTrackerAccount.objects.create(user_id="1", person=person, tracker=tracker, is_developer=True)
1740 BugComment.objects.create(bug=bug, account=account, comment_id="12345", url='https://jira.com/browse/12345',
1741 created_on=comment_created)
1742 comm_list = j._Jira__poll_comments(bug, issue)
1743 result = []
1744 for c in comm_list:
1745 result.append((c.db_object.account.person.full_name,
1746 c.body,
1747 c.db_object.created_on))
1749 self.assertEqual(result, exp_resp)
1751 @patch('CIResults.bugtrackers.Jira.jira')
1752 def test_search_bugs_ids__full(self, connection_mock):
1753 tracker = BugTracker.objects.create(tracker_type="jira", project='PRODUCT',
1754 public=True)
1755 jira = Jira(tracker)
1757 # Mock the return value of the search command
1758 JiraBug = namedtuple('JiraBug', ('key', ))
1760 bugs1 = []
1761 for i in range(1000):
1762 bugs1.append(JiraBug(key=f"PRODUCT-{i}"))
1764 bugs2 = []
1765 for i in range(1000, 2000):
1766 bugs2.append(JiraBug(key=f"PRODUCT-{i}"))
1768 bugs3 = []
1769 for i in range(1000, 2000):
1770 bugs3.append(JiraBug(key=f"PRODUCT-{i}"))
1772 connection_mock.search_issues.side_effect = [ResultList(bugs1, _total=2500),
1773 ResultList(bugs2, _total=2500),
1774 ResultList(bugs3, _total=2500)]
1776 # Get the list of open bugs
1777 updated_since = datetime.datetime.fromtimestamp(125)
1778 open_bugs = jira.search_bugs_ids(components=["COMPONENT1", "COMPONENT2"],
1779 created_since=datetime.datetime.fromtimestamp(125),
1780 updated_since=updated_since,
1781 status=['status1', 'status2'])
1782 self.assertEqual(open_bugs, set([b.key for b in bugs1+bugs2+bugs3]))
1784 # Verify that the request was valid
1785 connection_mock.search_issues.assert_any_call('project = \'PRODUCT\' '
1786 'AND component in ("COMPONENT1", "COMPONENT2") '
1787 'AND created > "1970/01/01 00:02" '
1788 'AND updated > "1970/01/01 00:02" '
1789 'AND status in ("status1", "status2")',
1790 fields=['key'], maxResults=1000)
1791 connection_mock.search_issues.assert_any_call('project = \'PRODUCT\' '
1792 'AND component in ("COMPONENT1", "COMPONENT2") '
1793 'AND created > "1970/01/01 00:02" '
1794 'AND updated > "1970/01/01 00:02" '
1795 'AND status in ("status1", "status2")',
1796 fields=['key'], maxResults=1000, startAt=1000)
1797 connection_mock.search_issues.assert_any_call('project = \'PRODUCT\' '
1798 'AND component in ("COMPONENT1", "COMPONENT2") '
1799 'AND created > "1970/01/01 00:02" '
1800 'AND updated > "1970/01/01 00:02" '
1801 'AND status in ("status1", "status2")',
1802 fields=['key'], maxResults=1000, startAt=2000)
1804 @patch('CIResults.bugtrackers.Jira.jira')
1805 def test_open_statuses(self, j_mock):
1806 def statuses():
1807 stat1_cat = MagicMock()
1808 stat1_cat.name = "To Do"
1809 stat1 = MagicMock(statusCategory=stat1_cat)
1810 stat1.name = "Open"
1812 stat2_cat = MagicMock()
1813 stat2_cat.name = "In Progress"
1814 stat2 = MagicMock(statusCategory=stat2_cat)
1815 stat2.name = "Scoping"
1817 stat3_cat = MagicMock()
1818 stat3_cat.name = "Done"
1819 stat3 = MagicMock(statusCategory=stat3_cat)
1820 stat3.name = "Closed"
1822 return [stat1, stat2, stat3]
1824 j_mock.statuses = statuses
1825 tracker = BugTracker.objects.create(tracker_type="jira", project='PRODUCT',
1826 public=True)
1827 jira = Jira(tracker)
1828 self.assertEqual(jira.open_statuses, ['Open', 'Scoping'])
1830 @patch('CIResults.bugtrackers.Jira.jira')
1831 def test_add_comment(self, connection_mock):
1832 tracker = BugTracker.objects.create(tracker_type="jira", public=True)
1833 jira = Jira(tracker)
1835 jira.add_comment(Bug(tracker=tracker, bug_id="JIRA-123"), "My comment")
1837 connection_mock.add_comment.assert_called_with("JIRA-123", "My comment")
1839 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
1840 def test_create_bug_from_json(self):
1841 tracker = BugTracker.objects.create(tracker_type="jira", url="https://foo",
1842 project=JiraMock.PROJECT_KEY,
1843 public=True)
1844 j = Jira(tracker)
1845 resp = MagicMock(key=1)
1846 JiraMock.create_issue = MagicMock(return_value=resp)
1847 summary = "summary"
1848 description = "description"
1849 status = {"name": "FOO"}
1850 components = [{"name": "BAR"}, {"name": "Blah"}]
1851 json_bug = {'summary': summary,
1852 'description': description,
1853 'status': status,
1854 'components': components,
1855 'issue_type': {'name': "Bug"}}
1857 with patch('CIResults.bugtrackers.Jira.transition') as t_mock:
1858 j.create_bug_from_json(json_bug)
1859 t_mock.assert_not_called()
1860 args, kwargs = JiraMock.create_issue.call_args_list[0]
1861 request = kwargs['fields']
1862 self.assertEqual(request['project'], {'key': JiraMock.PROJECT_KEY})
1863 del (request['project'])
1864 self.assertEqual(request['issuetype'], {'name': "Bug"})
1865 del (request['issuetype'])
1866 for field in json_bug:
1867 self.assertEqual(request[field], json_bug[field])
1869 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
1870 def test_create_bug_from_json_title(self):
1871 tracker = BugTracker.objects.create(tracker_type="jira", url="https://foo",
1872 project=JiraMock.PROJECT_KEY,
1873 public=True)
1874 j = Jira(tracker)
1875 resp = MagicMock(key=1)
1876 JiraMock.create_issue = MagicMock(return_value=resp)
1877 summary = "summary"
1878 description = "description"
1879 status = {"name": "FOO"}
1880 components = [{"name": "BAR"}, {"name": "Blah"}]
1881 json_bug = {'title': summary,
1882 'description': description,
1883 'status': status,
1884 'components': components}
1886 j.create_bug_from_json(json_bug)
1887 args, kwargs = JiraMock.create_issue.call_args_list[0]
1888 request = kwargs['fields']
1889 self.assertEqual(request['summary'], summary)
1891 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
1892 def test_create_bug_from_json_issuetype(self):
1893 tracker = BugTracker.objects.create(tracker_type="jira", url="https://foo",
1894 project=JiraMock.PROJECT_KEY,
1895 public=True)
1896 j = Jira(tracker)
1897 resp = MagicMock(key=1)
1898 JiraMock.create_issue = MagicMock(return_value=resp)
1899 summary = "summary"
1900 description = "description"
1901 status = {"name": "FOO"}
1902 components = [{"name": "BAR"}, {"name": "Blah"}]
1903 json_bug = {'title': summary,
1904 'description': description,
1905 'status': status,
1906 'components': components}
1908 j.create_bug_from_json(json_bug)
1909 args, kwargs = JiraMock.create_issue.call_args_list[0]
1910 request = kwargs['fields']
1911 self.assertEqual(request['issuetype'], {'name': "Bug"})
1913 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
1914 def test_create_bug_from_json_error(self):
1915 tracker = BugTracker.objects.create(tracker_type="jira", url="https://foo",
1916 project=JiraMock.PROJECT_KEY,
1917 public=True)
1918 j = Jira(tracker)
1919 JiraMock.create_issue = MagicMock(side_effect=JIRAError)
1920 summary = "summary"
1921 description = "description"
1922 status = {"name": "FOO"}
1923 components = [{"name": "BAR"}, {"name": "Blah"}]
1924 json_bug = {'title': summary,
1925 'description': description,
1926 'status': status,
1927 'components': components}
1929 with self.assertRaises(ValueError):
1930 j.create_bug_from_json(json_bug)
1932 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
1933 def test_update_bug_from_json(self):
1934 tracker = BugTracker.objects.create(tracker_type="jira", url="https://foo",
1935 project=JiraMock.PROJECT_KEY,
1936 public=True)
1937 j = Jira(tracker)
1938 JiraMock.issue = MagicMock()
1939 JiraMock.issue.return_value.update = MagicMock()
1940 JiraMock.transition_issue = MagicMock()
1941 summary = "summary"
1942 status = {"name": "FOO"}
1943 json_bug = {'summary': summary,
1944 'status': status}
1946 j.update_bug_from_json(json_bug, 1)
1947 args, kwargs = JiraMock.issue.return_value.update.call_args_list[0]
1948 request = kwargs['fields']
1949 self.assertEqual(request['project'], {'key': JiraMock.PROJECT_KEY})
1951 del (request['project'])
1952 for field in json_bug:
1953 self.assertEqual(request[field], json_bug[field])
1954 JiraMock.transition_issue.assert_not_called()
1956 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
1957 def test_update_bug_from_json_error(self):
1958 tracker = BugTracker.objects.create(tracker_type="jira", url="https://foo",
1959 project=JiraMock.PROJECT_KEY,
1960 public=True)
1961 j = Jira(tracker)
1962 JiraMock.issue = MagicMock()
1963 JiraMock.issue.return_value.update = MagicMock(side_effect=JIRAError)
1965 with self.assertRaises(ValueError):
1966 j.update_bug_from_json({"foo": "bar"}, 1)
1968 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
1969 def test_update_bug_from_json_transition(self):
1970 tracker = BugTracker.objects.create(tracker_type="jira", url="https://foo",
1971 project=JiraMock.PROJECT_KEY,
1972 public=True)
1973 j = Jira(tracker)
1974 issue = MagicMock()
1975 JiraMock.issue = MagicMock()
1976 JiraMock.issue.return_value = issue
1977 issue.update = MagicMock()
1978 j.transition = MagicMock()
1979 json_bug = {'transition': {'status': "Open", 'fields': {'resolution': 'In Progress'}}}
1981 j.update_bug_from_json(json_bug, 1)
1982 j.transition.assert_called_with(1, "Open", {'resolution': "In Progress"})
1984 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
1985 def test_update_bug_from_json_update_field(self):
1986 tracker = BugTracker.objects.create(tracker_type="jira", url="https://foo",
1987 project=JiraMock.PROJECT_KEY,
1988 public=True)
1989 j = Jira(tracker)
1990 issue = MagicMock()
1991 JiraMock.issue = MagicMock()
1992 JiraMock.issue.return_value = issue
1993 issue.update = MagicMock()
1994 json_bug = {'update': {'labels': [{'add': 'foo'}]}, 'summary': "Foo"}
1996 j.update_bug_from_json(json_bug, 1)
1997 issue.update.assert_called_with(update={'labels': [{'add': 'foo'}]},
1998 fields={'summary': "Foo", 'project': {'key': "TEST"}})
2000 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
2001 def test_transition(self):
2002 tracker = BugTracker.objects.create(tracker_type="jira", url="https://foo",
2003 project=JiraMock.PROJECT_KEY,
2004 public=True)
2005 j = Jira(tracker)
2006 issue = MagicMock()
2007 JiraMock.issue = MagicMock()
2008 JiraMock.issue.return_value = issue
2009 JiraMock.transition_issue = MagicMock()
2010 status = "Open"
2011 fields = {'resolution': 'In Progress'}
2013 j.transition(1, status, fields)
2014 JiraMock.transition_issue.assert_called_with(1, "Open", fields={'resolution': "In Progress"})
2016 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
2017 def test_transition_error(self):
2018 tracker = BugTracker.objects.create(tracker_type="jira", url="https://foo",
2019 project=JiraMock.PROJECT_KEY,
2020 public=True)
2021 j = Jira(tracker)
2022 issue = MagicMock()
2023 JiraMock.issue = MagicMock()
2024 JiraMock.issue.return_value = issue
2025 JiraMock.transition_issue = MagicMock(side_effect=JIRAError)
2026 status = "Open"
2027 fields = {'resolution': 'In Progress'}
2029 with self.assertRaises(ValueError):
2030 j.transition(1, status, fields)
2032 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
2033 def test_transition_create_bug_from_json(self):
2034 tracker = BugTracker.objects.create(tracker_type="jira", url="https://foo",
2035 project=JiraMock.PROJECT_KEY,
2036 public=True)
2037 j = Jira(tracker)
2038 resp = MagicMock(key=1)
2039 issue = MagicMock()
2040 JiraMock.issue = MagicMock()
2041 JiraMock.issue.return_value = issue
2042 JiraMock.create_issue = MagicMock(return_value=resp)
2043 j.transition = MagicMock()
2044 json_bug = {'summary': "FOO", 'transition': {'status': "Closed", 'fields': {'resolution': 'Fixed'}}}
2046 bug_id = j.create_bug_from_json(json_bug)
2047 self.assertEqual(bug_id, 1)
2048 j.transition.assert_called_with(1, "Closed", {'resolution': "Fixed"})
2050 @patch('CIResults.bugtrackers.Jira.jira', JiraMock)
2051 def test_transition_create_bug_from_json_error(self):
2052 tracker = BugTracker.objects.create(tracker_type="jira", url="https://foo",
2053 project=JiraMock.PROJECT_KEY,
2054 public=True)
2055 j = Jira(tracker)
2056 resp = MagicMock(key=1)
2057 JiraMock.create_issue = MagicMock(return_value=resp)
2058 j.transition = MagicMock(side_effect=ValueError)
2059 json_bug = {"title": "bar", "transition": {"status": "Open"}}
2061 bug_id = j.create_bug_from_json(json_bug)
2062 self.assertEqual(bug_id, 1)
2065class BugTrackerJiraUntrackedTests(TestCase):
2066 def setUp(self):
2067 self.db_tracker = BugTracker.objects.create(tracker_type="jira_untracked",
2068 project='PROJECT', public=True)
2070 def test_poll(self):
2071 bug = MagicMock(spec=Bug)
2072 Untracked(self.db_tracker).poll(bug)
2074 self.assertEqual(bug.title, "UNKNOWN")
2075 self.assertEqual(bug.status, "UNKNOWN")
2076 bug.save.assert_not_called()
2078 def test_search_bugs_ids(self):
2079 self.assertEqual(Untracked(None).search_bugs_ids(), set())
2081 def test_open_statuses(self):
2082 self.assertEqual(Untracked(None).open_statuses, [])
2084 def test_add_comment(self):
2085 self.assertEqual(Untracked(None).add_comment(Bug(bug_id="1234"), "Hello World"), None)
2087 def test_create_bug(self):
2088 self.assertEqual(Untracked(self.db_tracker).create_bug(Bug(tracker=self.db_tracker)), None)