blob: 4f89cc9df2de0486a7f421972615a4dff665b393 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2016 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""Unittest for the tracker helpers module."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import copy
12import mock
13import unittest
14
15import settings
16
17from businesslogic import work_env
18from framework import exceptions
19from framework import framework_constants
20from framework import framework_helpers
21from framework import permissions
22from framework import template_helpers
23from framework import urls
24from proto import project_pb2
25from proto import tracker_pb2
26from proto import user_pb2
27from services import service_manager
28from testing import fake
29from testing import testing_helpers
30from tracker import tracker_bizobj
31from tracker import tracker_constants
32from tracker import tracker_helpers
33
34TEST_ID_MAP = {
35 'a@example.com': 1,
36 'b@example.com': 2,
37 'c@example.com': 3,
38 'd@example.com': 4,
39 }
40
41
42def _Issue(project_name, local_id, summary='', status='', project_id=789):
43 issue = tracker_pb2.Issue()
44 issue.project_name = project_name
45 issue.project_id = project_id
46 issue.local_id = local_id
47 issue.issue_id = 100000 + local_id
48 issue.summary = summary
49 issue.status = status
50 return issue
51
52
53def _MakeConfig():
54 config = tracker_pb2.ProjectIssueConfig()
55 config.well_known_statuses.append(tracker_pb2.StatusDef(
56 means_open=True, status='New', deprecated=False))
57 config.well_known_statuses.append(tracker_pb2.StatusDef(
58 status='Old', means_open=False, deprecated=False))
59 config.well_known_statuses.append(tracker_pb2.StatusDef(
60 status='StatusThatWeDontUseAnymore', means_open=False, deprecated=True))
61
62 return config
63
64
65class HelpersTest(unittest.TestCase):
66
67 def setUp(self):
68 self.services = service_manager.Services(
69 project=fake.ProjectService(),
70 config=fake.ConfigService(),
71 issue=fake.IssueService(),
72 user=fake.UserService(),
73 usergroup=fake.UserGroupService())
74
75 for email, user_id in TEST_ID_MAP.items():
76 self.services.user.TestAddUser(email, user_id)
77
78 self.services.project.TestAddProject('testproj', project_id=789)
79 self.issue1 = fake.MakeTestIssue(789, 1, 'one', 'New', 111)
80 self.issue1.project_name = 'testproj'
81 self.services.issue.TestAddIssue(self.issue1)
82 self.issue2 = fake.MakeTestIssue(789, 2, 'two', 'New', 111)
83 self.issue2.project_name = 'testproj'
84 self.services.issue.TestAddIssue(self.issue2)
85 self.issue3 = fake.MakeTestIssue(789, 3, 'three', 'New', 111)
86 self.issue3.project_name = 'testproj'
87 self.services.issue.TestAddIssue(self.issue3)
88 self.cnxn = 'fake connextion'
89 self.errors = template_helpers.EZTError()
90 self.default_colspec_param = 'colspec=%s' % (
91 tracker_constants.DEFAULT_COL_SPEC.replace(' ', '%20'))
92 self.services.usergroup.TestAddGroupSettings(999, 'group@example.com')
93
94 def testParseIssueRequest_Empty(self):
95 post_data = fake.PostData()
96 errors = template_helpers.EZTError()
97 parsed = tracker_helpers.ParseIssueRequest(
98 'fake cnxn', post_data, self.services, errors, 'proj')
99 self.assertEqual('', parsed.summary)
100 self.assertEqual('', parsed.comment)
101 self.assertEqual('', parsed.status)
102 self.assertEqual('', parsed.users.owner_username)
103 self.assertEqual(0, parsed.users.owner_id)
104 self.assertEqual([], parsed.users.cc_usernames)
105 self.assertEqual([], parsed.users.cc_usernames_remove)
106 self.assertEqual([], parsed.users.cc_ids)
107 self.assertEqual([], parsed.users.cc_ids_remove)
108 self.assertEqual('', parsed.template_name)
109 self.assertEqual([], parsed.labels)
110 self.assertEqual([], parsed.labels_remove)
111 self.assertEqual({}, parsed.fields.vals)
112 self.assertEqual({}, parsed.fields.vals_remove)
113 self.assertEqual([], parsed.fields.fields_clear)
114 self.assertEqual('', parsed.blocked_on.entered_str)
115 self.assertEqual([], parsed.blocked_on.iids)
116
117 def testParseIssueRequest_Normal(self):
118 post_data = fake.PostData({
119 'summary': ['some summary'],
120 'comment': ['some comment'],
121 'status': ['SomeStatus'],
122 'template_name': ['some template'],
123 'label': ['lab1', '-lab2'],
124 'custom_123': ['field1123a', 'field1123b'],
125 })
126 errors = template_helpers.EZTError()
127 parsed = tracker_helpers.ParseIssueRequest(
128 'fake cnxn', post_data, self.services, errors, 'proj')
129 self.assertEqual('some summary', parsed.summary)
130 self.assertEqual('some comment', parsed.comment)
131 self.assertEqual('SomeStatus', parsed.status)
132 self.assertEqual('', parsed.users.owner_username)
133 self.assertEqual(0, parsed.users.owner_id)
134 self.assertEqual([], parsed.users.cc_usernames)
135 self.assertEqual([], parsed.users.cc_usernames_remove)
136 self.assertEqual([], parsed.users.cc_ids)
137 self.assertEqual([], parsed.users.cc_ids_remove)
138 self.assertEqual('some template', parsed.template_name)
139 self.assertEqual(['lab1'], parsed.labels)
140 self.assertEqual(['lab2'], parsed.labels_remove)
141 self.assertEqual({123: ['field1123a', 'field1123b']}, parsed.fields.vals)
142 self.assertEqual({}, parsed.fields.vals_remove)
143 self.assertEqual([], parsed.fields.fields_clear)
144
145 def testMarkupDescriptionOnInput(self):
146 content = 'What?\nthat\nWhy?\nidk\nWhere?\n'
147 tmpl_txt = 'What?\nWhy?\nWhere?\nWhen?'
148 desc = '<b>What?</b>\nthat\n<b>Why?</b>\nidk\n<b>Where?</b>\n'
149 self.assertEqual(tracker_helpers.MarkupDescriptionOnInput(
150 content, tmpl_txt), desc)
151
152 def testMarkupDescriptionLineOnInput(self):
153 line = 'What happened??'
154 tmpl_lines = ['What happened??','Why?']
155 self.assertEqual(tracker_helpers._MarkupDescriptionLineOnInput(
156 line, tmpl_lines), '<b>What happened??</b>')
157
158 line = 'Something terrible!!!'
159 self.assertEqual(tracker_helpers._MarkupDescriptionLineOnInput(
160 line, tmpl_lines), 'Something terrible!!!')
161
162 def testClassifyPlusMinusItems(self):
163 add, remove = tracker_helpers._ClassifyPlusMinusItems([])
164 self.assertEqual([], add)
165 self.assertEqual([], remove)
166
167 add, remove = tracker_helpers._ClassifyPlusMinusItems(
168 ['', ' ', ' \t', '-'])
169 self.assertItemsEqual([], add)
170 self.assertItemsEqual([], remove)
171
172 add, remove = tracker_helpers._ClassifyPlusMinusItems(
173 ['a', 'b', 'c'])
174 self.assertItemsEqual(['a', 'b', 'c'], add)
175 self.assertItemsEqual([], remove)
176
177 add, remove = tracker_helpers._ClassifyPlusMinusItems(
178 ['a-a-a', 'b-b', 'c-'])
179 self.assertItemsEqual(['a-a-a', 'b-b', 'c-'], add)
180 self.assertItemsEqual([], remove)
181
182 add, remove = tracker_helpers._ClassifyPlusMinusItems(
183 ['-a'])
184 self.assertItemsEqual([], add)
185 self.assertItemsEqual(['a'], remove)
186
187 add, remove = tracker_helpers._ClassifyPlusMinusItems(
188 ['-a', 'b', 'c-c'])
189 self.assertItemsEqual(['b', 'c-c'], add)
190 self.assertItemsEqual(['a'], remove)
191
192 add, remove = tracker_helpers._ClassifyPlusMinusItems(
193 ['-a', '-b-b', '-c-'])
194 self.assertItemsEqual([], add)
195 self.assertItemsEqual(['a', 'b-b', 'c-'], remove)
196
197 # We dedup, but we don't cancel out items that are both added and removed.
198 add, remove = tracker_helpers._ClassifyPlusMinusItems(
199 ['a', 'a', '-a'])
200 self.assertItemsEqual(['a'], add)
201 self.assertItemsEqual(['a'], remove)
202
203 def testParseIssueRequestFields(self):
204 parsed_fields = tracker_helpers._ParseIssueRequestFields(fake.PostData({
205 'custom_1': ['https://hello.com'],
206 'custom_12': ['https://blah.com'],
207 'custom_14': ['https://remove.com'],
208 'custom_15_goats': ['2', '3'],
209 'custom_15_sheep': ['3', '5'],
210 'custom_16_sheep': ['yarn'],
211 'op_custom_14': ['remove'],
212 'op_custom_12': ['clear'],
213 'op_custom_16_sheep': ['remove'],
214 'ignore': 'no matter',}))
215 self.assertEqual(
216 parsed_fields,
217 tracker_helpers.ParsedFields(
218 {
219 1: ['https://hello.com'],
220 12: ['https://blah.com']
221 }, {14: ['https://remove.com']}, [12],
222 {15: {
223 'goats': ['2', '3'],
224 'sheep': ['3', '5']
225 }}, {16: {
226 'sheep': ['yarn']
227 }}))
228
229 def testParseIssueRequestAttachments(self):
230 file1 = testing_helpers.Blank(
231 filename='hello.c',
232 value='hello world')
233
234 file2 = testing_helpers.Blank(
235 filename='README',
236 value='Welcome to our project')
237
238 file3 = testing_helpers.Blank(
239 filename='c:\\dir\\subdir\\FILENAME.EXT',
240 value='Abort, Retry, or Fail?')
241
242 # Browsers send this if FILE field was not filled in.
243 file4 = testing_helpers.Blank(
244 filename='',
245 value='')
246
247 attachments = tracker_helpers._ParseIssueRequestAttachments({})
248 self.assertEqual([], attachments)
249
250 attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
251 'file1': [file1],
252 }))
253 self.assertEqual(
254 [('hello.c', 'hello world', 'text/plain')],
255 attachments)
256
257 attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
258 'file1': [file1],
259 'file2': [file2],
260 }))
261 self.assertEqual(
262 [('hello.c', 'hello world', 'text/plain'),
263 ('README', 'Welcome to our project', 'text/plain')],
264 attachments)
265
266 attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
267 'file3': [file3],
268 }))
269 self.assertEqual(
270 [('FILENAME.EXT', 'Abort, Retry, or Fail?',
271 'application/octet-stream')],
272 attachments)
273
274 attachments = tracker_helpers._ParseIssueRequestAttachments(fake.PostData({
275 'file1': [file4], # Does not appear in result
276 'file3': [file3],
277 'file4': [file4], # Does not appear in result
278 }))
279 self.assertEqual(
280 [('FILENAME.EXT', 'Abort, Retry, or Fail?',
281 'application/octet-stream')],
282 attachments)
283
284 def testParseIssueRequestKeptAttachments(self):
285 pass # TODO(jrobbins): Write this test.
286
287 def testParseIssueRequestUsers(self):
288 post_data = {}
289 parsed_users = tracker_helpers._ParseIssueRequestUsers(
290 'fake connection', post_data, self.services)
291 self.assertEqual('', parsed_users.owner_username)
292 self.assertEqual(
293 framework_constants.NO_USER_SPECIFIED, parsed_users.owner_id)
294 self.assertEqual([], parsed_users.cc_usernames)
295 self.assertEqual([], parsed_users.cc_usernames_remove)
296 self.assertEqual([], parsed_users.cc_ids)
297 self.assertEqual([], parsed_users.cc_ids_remove)
298
299 post_data = fake.PostData({
300 'owner': [''],
301 })
302 parsed_users = tracker_helpers._ParseIssueRequestUsers(
303 'fake connection', post_data, self.services)
304 self.assertEqual('', parsed_users.owner_username)
305 self.assertEqual(
306 framework_constants.NO_USER_SPECIFIED, parsed_users.owner_id)
307 self.assertEqual([], parsed_users.cc_usernames)
308 self.assertEqual([], parsed_users.cc_usernames_remove)
309 self.assertEqual([], parsed_users.cc_ids)
310 self.assertEqual([], parsed_users.cc_ids_remove)
311
312 post_data = fake.PostData({
313 'owner': [' \t'],
314 })
315 parsed_users = tracker_helpers._ParseIssueRequestUsers(
316 'fake connection', post_data, self.services)
317 self.assertEqual('', parsed_users.owner_username)
318 self.assertEqual(
319 framework_constants.NO_USER_SPECIFIED, parsed_users.owner_id)
320 self.assertEqual([], parsed_users.cc_usernames)
321 self.assertEqual([], parsed_users.cc_usernames_remove)
322 self.assertEqual([], parsed_users.cc_ids)
323 self.assertEqual([], parsed_users.cc_ids_remove)
324
325 post_data = fake.PostData({
326 'owner': ['b@example.com'],
327 })
328 parsed_users = tracker_helpers._ParseIssueRequestUsers(
329 'fake connection', post_data, self.services)
330 self.assertEqual('b@example.com', parsed_users.owner_username)
331 self.assertEqual(TEST_ID_MAP['b@example.com'], parsed_users.owner_id)
332 self.assertEqual([], parsed_users.cc_usernames)
333 self.assertEqual([], parsed_users.cc_usernames_remove)
334 self.assertEqual([], parsed_users.cc_ids)
335 self.assertEqual([], parsed_users.cc_ids_remove)
336
337 post_data = fake.PostData({
338 'owner': ['b@example.com'],
339 })
340 parsed_users = tracker_helpers._ParseIssueRequestUsers(
341 'fake connection', post_data, self.services)
342 self.assertEqual('b@example.com', parsed_users.owner_username)
343 self.assertEqual(TEST_ID_MAP['b@example.com'], parsed_users.owner_id)
344 self.assertEqual([], parsed_users.cc_usernames)
345 self.assertEqual([], parsed_users.cc_usernames_remove)
346 self.assertEqual([], parsed_users.cc_ids)
347 self.assertEqual([], parsed_users.cc_ids_remove)
348
349 post_data = fake.PostData({
350 'cc': ['b@example.com'],
351 })
352 parsed_users = tracker_helpers._ParseIssueRequestUsers(
353 'fake connection', post_data, self.services)
354 self.assertEqual('', parsed_users.owner_username)
355 self.assertEqual(
356 framework_constants.NO_USER_SPECIFIED, parsed_users.owner_id)
357 self.assertEqual(['b@example.com'], parsed_users.cc_usernames)
358 self.assertEqual([], parsed_users.cc_usernames_remove)
359 self.assertEqual([TEST_ID_MAP['b@example.com']], parsed_users.cc_ids)
360 self.assertEqual([], parsed_users.cc_ids_remove)
361
362 post_data = fake.PostData({
363 'cc': ['-b@example.com, c@example.com,,'
364 'a@example.com,'],
365 })
366 parsed_users = tracker_helpers._ParseIssueRequestUsers(
367 'fake connection', post_data, self.services)
368 self.assertEqual('', parsed_users.owner_username)
369 self.assertEqual(
370 framework_constants.NO_USER_SPECIFIED, parsed_users.owner_id)
371 self.assertItemsEqual(['c@example.com', 'a@example.com'],
372 parsed_users.cc_usernames)
373 self.assertEqual(['b@example.com'], parsed_users.cc_usernames_remove)
374 self.assertItemsEqual([TEST_ID_MAP['c@example.com'],
375 TEST_ID_MAP['a@example.com']],
376 parsed_users.cc_ids)
377 self.assertEqual([TEST_ID_MAP['b@example.com']],
378 parsed_users.cc_ids_remove)
379
380 post_data = fake.PostData({
381 'owner': ['fuhqwhgads@example.com'],
382 'cc': ['c@example.com, fuhqwhgads@example.com'],
383 })
384 parsed_users = tracker_helpers._ParseIssueRequestUsers(
385 'fake connection', post_data, self.services)
386 self.assertEqual('fuhqwhgads@example.com', parsed_users.owner_username)
387 gen_uid = framework_helpers.MurmurHash3_x86_32(parsed_users.owner_username)
388 self.assertEqual(gen_uid, parsed_users.owner_id) # autocreated user
389 self.assertItemsEqual(
390 ['c@example.com', 'fuhqwhgads@example.com'], parsed_users.cc_usernames)
391 self.assertEqual([], parsed_users.cc_usernames_remove)
392 self.assertItemsEqual(
393 [TEST_ID_MAP['c@example.com'], gen_uid], parsed_users.cc_ids)
394 self.assertEqual([], parsed_users.cc_ids_remove)
395
396 post_data = fake.PostData({
397 'cc': ['C@example.com, b@exAmple.cOm'],
398 })
399 parsed_users = tracker_helpers._ParseIssueRequestUsers(
400 'fake connection', post_data, self.services)
401 self.assertItemsEqual(
402 ['c@example.com', 'b@example.com'], parsed_users.cc_usernames)
403 self.assertEqual([], parsed_users.cc_usernames_remove)
404 self.assertItemsEqual(
405 [TEST_ID_MAP['c@example.com'], TEST_ID_MAP['b@example.com']],
406 parsed_users.cc_ids)
407 self.assertEqual([], parsed_users.cc_ids_remove)
408
409 def testParseBlockers_BlockedOnNothing(self):
410 """Was blocked on nothing, still nothing."""
411 post_data = {tracker_helpers.BLOCKED_ON: ''}
412 parsed_blockers = tracker_helpers._ParseBlockers(
413 self.cnxn, post_data, self.services, self.errors, 'testproj',
414 tracker_helpers.BLOCKED_ON)
415
416 self.assertEqual('', parsed_blockers.entered_str)
417 self.assertEqual([], parsed_blockers.iids)
418 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
419 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKING))
420
421 def testParseBlockers_BlockedOnAdded(self):
422 """Was blocked on nothing; now 1, 2, 3."""
423 post_data = {tracker_helpers.BLOCKED_ON: '1, 2, 3'}
424 parsed_blockers = tracker_helpers._ParseBlockers(
425 self.cnxn, post_data, self.services, self.errors, 'testproj',
426 tracker_helpers.BLOCKED_ON)
427
428 self.assertEqual('1, 2, 3', parsed_blockers.entered_str)
429 self.assertEqual([100001, 100002, 100003], parsed_blockers.iids)
430 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
431 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKING))
432
433 def testParseBlockers_BlockedOnDuplicateRef(self):
434 """Was blocked on nothing; now just 2, but repeated in input."""
435 post_data = {tracker_helpers.BLOCKED_ON: '2, 2, 2'}
436 parsed_blockers = tracker_helpers._ParseBlockers(
437 self.cnxn, post_data, self.services, self.errors, 'testproj',
438 tracker_helpers.BLOCKED_ON)
439
440 self.assertEqual('2, 2, 2', parsed_blockers.entered_str)
441 self.assertEqual([100002], parsed_blockers.iids)
442 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
443 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKING))
444
445 def testParseBlockers_Missing(self):
446 """Parsing an input field that was not in the POST."""
447 post_data = {}
448 parsed_blockers = tracker_helpers._ParseBlockers(
449 self.cnxn, post_data, self.services, self.errors, 'testproj',
450 tracker_helpers.BLOCKED_ON)
451
452 self.assertEqual('', parsed_blockers.entered_str)
453 self.assertEqual([], parsed_blockers.iids)
454 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
455 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKING))
456
457 def testParseBlockers_SameIssueNoProject(self):
458 """Adding same issue as blocker should modify the errors object."""
459 post_data = {'id': '2', tracker_helpers.BLOCKING: '2, 3'}
460
461 parsed_blockers = tracker_helpers._ParseBlockers(
462 self.cnxn, post_data, self.services, self.errors, 'testproj',
463 tracker_helpers.BLOCKING)
464 self.assertEqual('2, 3', parsed_blockers.entered_str)
465 self.assertEqual([], parsed_blockers.iids)
466 self.assertEqual(
467 getattr(self.errors, tracker_helpers.BLOCKING),
468 'Cannot be blocking the same issue')
469 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
470
471 def testParseBlockers_SameIssueSameProject(self):
472 """Adding same issue as blocker should modify the errors object."""
473 post_data = {'id': '2', tracker_helpers.BLOCKING: 'testproj:2, 3'}
474
475 parsed_blockers = tracker_helpers._ParseBlockers(
476 self.cnxn, post_data, self.services, self.errors, 'testproj',
477 tracker_helpers.BLOCKING)
478 self.assertEqual('testproj:2, 3', parsed_blockers.entered_str)
479 self.assertEqual([], parsed_blockers.iids)
480 self.assertEqual(
481 getattr(self.errors, tracker_helpers.BLOCKING),
482 'Cannot be blocking the same issue')
483 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
484
485 def testParseBlockers_SameIssueDifferentProject(self):
486 """Adding different blocker issue should not modify the errors object."""
487 post_data = {'id': '2', tracker_helpers.BLOCKING: 'testproj:2'}
488
489 parsed_blockers = tracker_helpers._ParseBlockers(
490 self.cnxn, post_data, self.services, self.errors, 'testprojB',
491 tracker_helpers.BLOCKING)
492 self.assertEqual('testproj:2', parsed_blockers.entered_str)
493 self.assertEqual([100002], parsed_blockers.iids)
494 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKING))
495 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
496
497 def testParseBlockers_Invalid(self):
498 """Input fields with invalid values should modify the errors object."""
499 post_data = {tracker_helpers.BLOCKING: '2, foo',
500 tracker_helpers.BLOCKED_ON: '3, bar'}
501
502 parsed_blockers = tracker_helpers._ParseBlockers(
503 self.cnxn, post_data, self.services, self.errors, 'testproj',
504 tracker_helpers.BLOCKING)
505 self.assertEqual('2, foo', parsed_blockers.entered_str)
506 self.assertEqual([100002], parsed_blockers.iids)
507 self.assertEqual(
508 getattr(self.errors, tracker_helpers.BLOCKING), 'Invalid issue ID foo')
509 self.assertIsNone(getattr(self.errors, tracker_helpers.BLOCKED_ON))
510
511 parsed_blockers = tracker_helpers._ParseBlockers(
512 self.cnxn, post_data, self.services, self.errors, 'testproj',
513 tracker_helpers.BLOCKED_ON)
514 self.assertEqual('3, bar', parsed_blockers.entered_str)
515 self.assertEqual([100003], parsed_blockers.iids)
516 self.assertEqual(
517 getattr(self.errors, tracker_helpers.BLOCKED_ON),
518 'Invalid issue ID bar')
519
520 def testParseBlockers_Dangling(self):
521 """A ref to a sanctioned projected should be allowed."""
522 post_data = {'id': '2', tracker_helpers.BLOCKING: 'otherproj:2'}
523 real_codesite_projects = settings.recognized_codesite_projects
524 settings.recognized_codesite_projects = ['otherproj']
525 parsed_blockers = tracker_helpers._ParseBlockers(
526 self.cnxn, post_data, self.services, self.errors, 'testproj',
527 tracker_helpers.BLOCKING)
528 self.assertEqual('otherproj:2', parsed_blockers.entered_str)
529 self.assertEqual([('otherproj', 2)], parsed_blockers.dangling_refs)
530 settings.recognized_codesite_projects = real_codesite_projects
531
532 def testParseBlockers_FederatedReferences(self):
533 """Should parse and return FedRefs."""
534 post_data = {'id': '9', tracker_helpers.BLOCKING: '2, b/123, 3, b/789'}
535 parsed_blockers = tracker_helpers._ParseBlockers(
536 self.cnxn, post_data, self.services, self.errors, 'testproj',
537 tracker_helpers.BLOCKING)
538 self.assertEqual('2, b/123, 3, b/789', parsed_blockers.entered_str)
539 self.assertEqual([100002, 100003], parsed_blockers.iids)
540 self.assertEqual(['b/123', 'b/789'], parsed_blockers.federated_ref_strings)
541
542 def testIsValidIssueOwner(self):
543 project = project_pb2.Project()
544 project.owner_ids.extend([1, 2])
545 project.committer_ids.extend([3])
546 project.contributor_ids.extend([4, 999])
547
548 valid, _ = tracker_helpers.IsValidIssueOwner(
549 'fake cnxn', project, framework_constants.NO_USER_SPECIFIED,
550 self.services)
551 self.assertTrue(valid)
552
553 valid, _ = tracker_helpers.IsValidIssueOwner(
554 'fake cnxn', project, 1,
555 self.services)
556 self.assertTrue(valid)
557 valid, _ = tracker_helpers.IsValidIssueOwner(
558 'fake cnxn', project, 2,
559 self.services)
560 self.assertTrue(valid)
561 valid, _ = tracker_helpers.IsValidIssueOwner(
562 'fake cnxn', project, 3,
563 self.services)
564 self.assertTrue(valid)
565 valid, _ = tracker_helpers.IsValidIssueOwner(
566 'fake cnxn', project, 4,
567 self.services)
568 self.assertTrue(valid)
569
570 valid, _ = tracker_helpers.IsValidIssueOwner(
571 'fake cnxn', project, 7,
572 self.services)
573 self.assertFalse(valid)
574
575 valid, _ = tracker_helpers.IsValidIssueOwner(
576 'fake cnxn', project, 999,
577 self.services)
578 self.assertFalse(valid)
579
580 # MakeViewsForUsersInIssuesTest is tested in MakeViewsForUsersInIssuesTest.
581
582 def testGetAllowedOpenedAndClosedIssues(self):
583 pass # TOOD(jrobbins): Write this test.
584
585 def testFormatIssueListURL_JumpedToIssue(self):
586 """If we jumped to issue 123, the list is can=1&q=id-123."""
587 config = tracker_pb2.ProjectIssueConfig()
588 path = '/p/proj/issues/detail?id=123&q=123'
589 mr = testing_helpers.MakeMonorailRequest(
590 path=path, headers={'Host': 'code.google.com'})
591 mr.ComputeColSpec(config)
592
593 absolute_base_url = 'http://code.google.com'
594
595 url_1 = tracker_helpers.FormatIssueListURL(mr, config)
596 self.assertEqual(
597 '%s/p/proj/issues/list?can=1&%s&q=id%%3D123' % (
598 absolute_base_url, self.default_colspec_param),
599 url_1)
600
601 def testFormatIssueListURL_NoCurrentState(self):
602 config = tracker_pb2.ProjectIssueConfig()
603 path = '/p/proj/issues/detail?id=123'
604 mr = testing_helpers.MakeMonorailRequest(
605 path=path, headers={'Host': 'code.google.com'})
606 mr.ComputeColSpec(config)
607
608 absolute_base_url = 'http://code.google.com'
609
610 url_1 = tracker_helpers.FormatIssueListURL(mr, config)
611 self.assertEqual(
612 '%s/p/proj/issues/list?%s&q=' % (
613 absolute_base_url, self.default_colspec_param),
614 url_1)
615
616 url_2 = tracker_helpers.FormatIssueListURL(
617 mr, config, foo=123)
618 self.assertEqual(
619 '%s/p/proj/issues/list?%s&foo=123&q=' % (
620 absolute_base_url, self.default_colspec_param),
621 url_2)
622
623 url_3 = tracker_helpers.FormatIssueListURL(
624 mr, config, foo=123, bar='abc')
625 self.assertEqual(
626 '%s/p/proj/issues/list?bar=abc&%s&foo=123&q=' % (
627 absolute_base_url, self.default_colspec_param),
628 url_3)
629
630 url_4 = tracker_helpers.FormatIssueListURL(
631 mr, config, baz='escaped+encoded&and100% "safe"')
632 self.assertEqual(
633 '%s/p/proj/issues/list?'
634 'baz=escaped%%2Bencoded%%26and100%%25%%20%%22safe%%22&%s&q=' % (
635 absolute_base_url, self.default_colspec_param),
636 url_4)
637
638 def testFormatIssueListURL_KeepCurrentState(self):
639 config = tracker_pb2.ProjectIssueConfig()
640 path = '/p/proj/issues/detail?id=123&sort=aa&colspec=a b c&groupby=d'
641 mr = testing_helpers.MakeMonorailRequest(
642 path=path, headers={'Host': 'localhost:8080'})
643 mr.ComputeColSpec(config)
644
645 absolute_base_url = 'http://localhost:8080'
646
647 url_1 = tracker_helpers.FormatIssueListURL(mr, config)
648 self.assertEqual(
649 '%s/p/proj/issues/list?colspec=a%%20b%%20c'
650 '&groupby=d&q=&sort=aa' % absolute_base_url,
651 url_1)
652
653 url_2 = tracker_helpers.FormatIssueListURL(
654 mr, config, foo=123)
655 self.assertEqual(
656 '%s/p/proj/issues/list?'
657 'colspec=a%%20b%%20c&foo=123&groupby=d&q=&sort=aa' % absolute_base_url,
658 url_2)
659
660 url_3 = tracker_helpers.FormatIssueListURL(
661 mr, config, colspec='X Y Z')
662 self.assertEqual(
663 '%s/p/proj/issues/list?colspec=a%%20b%%20c'
664 '&groupby=d&q=&sort=aa' % absolute_base_url,
665 url_3)
666
667 def testFormatRelativeIssueURL(self):
668 self.assertEqual(
669 '/p/proj/issues/attachment',
670 tracker_helpers.FormatRelativeIssueURL(
671 'proj', urls.ISSUE_ATTACHMENT))
672
673 self.assertEqual(
674 '/p/proj/issues/detail?id=123',
675 tracker_helpers.FormatRelativeIssueURL(
676 'proj', urls.ISSUE_DETAIL, id=123))
677
678 @mock.patch('google.appengine.api.app_identity.get_application_id')
679 def testFormatCrBugURL_Prod(self, mock_get_app_id):
680 mock_get_app_id.return_value = 'monorail-prod'
681 self.assertEqual(
682 'https://crbug.com/proj/123',
683 tracker_helpers.FormatCrBugURL('proj', 123))
684 self.assertEqual(
685 'https://crbug.com/123456',
686 tracker_helpers.FormatCrBugURL('chromium', 123456))
687
688 @mock.patch('google.appengine.api.app_identity.get_application_id')
689 def testFormatCrBugURL_NonProd(self, mock_get_app_id):
690 mock_get_app_id.return_value = 'monorail-staging'
691 self.assertEqual(
692 '/p/proj/issues/detail?id=123',
693 tracker_helpers.FormatCrBugURL('proj', 123))
694 self.assertEqual(
695 '/p/chromium/issues/detail?id=123456',
696 tracker_helpers.FormatCrBugURL('chromium', 123456))
697
698 @mock.patch('tracker.tracker_constants.ISSUE_ATTACHMENTS_QUOTA_HARD', 1)
699 def testComputeNewQuotaBytesUsed_ProjectQuota(self):
700 upload_1 = framework_helpers.AttachmentUpload(
701 'matter not', 'three men make a tiger', 'matter not')
702 upload_2 = framework_helpers.AttachmentUpload(
703 'matter not', 'chicken', 'matter not')
704 attachments = [upload_1, upload_2]
705
706 project = fake.Project()
707 project.attachment_bytes_used = 10
708 project.attachment_quota = project.attachment_bytes_used + len(
709 upload_1.contents + upload_2.contents) + 1
710
711 actual_new = tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
712 expected_new = project.attachment_quota - 1
713 self.assertEqual(actual_new, expected_new)
714
715 upload_3 = framework_helpers.AttachmentUpload(
716 'matter not', 'donut', 'matter not')
717 attachments.append(upload_3)
718 with self.assertRaises(exceptions.OverAttachmentQuota):
719 tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
720
721 @mock.patch(
722 'tracker.tracker_constants.ISSUE_ATTACHMENTS_QUOTA_HARD', len('tiger'))
723 def testComputeNewQuotaBytesUsed_GeneralQuota(self):
724 upload_1 = framework_helpers.AttachmentUpload(
725 'matter not', 'tiger', 'matter not')
726 attachments = [upload_1]
727
728 project = fake.Project()
729
730 actual_new = tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
731 expected_new = len(upload_1.contents)
732 self.assertEqual(actual_new, expected_new)
733
734 upload_2 = framework_helpers.AttachmentUpload(
735 'matter not', 'donut', 'matter not')
736 attachments.append(upload_2)
737 with self.assertRaises(exceptions.OverAttachmentQuota):
738 tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
739
740 upload_3 = framework_helpers.AttachmentUpload(
741 'matter not', 'donut', 'matter not')
742 attachments.append(upload_3)
743 with self.assertRaises(exceptions.OverAttachmentQuota):
744 tracker_helpers.ComputeNewQuotaBytesUsed(project, attachments)
745
746 def testIsUnderSoftAttachmentQuota(self):
747 pass # TODO(jrobbins): Write this test.
748
749 # GetAllIssueProjects is tested in GetAllIssueProjectsTest.
750
751 def testGetPermissionsInAllProjects(self):
752 pass # TODO(jrobbins): Write this test.
753
754 # FilterOutNonViewableIssues is tested in FilterOutNonViewableIssuesTest.
755
756 def testMeansOpenInProject(self):
757 config = _MakeConfig()
758
759 # ensure open means open
760 self.assertTrue(tracker_helpers.MeansOpenInProject('New', config))
761 self.assertTrue(tracker_helpers.MeansOpenInProject('new', config))
762
763 # ensure an unrecognized status means open
764 self.assertTrue(tracker_helpers.MeansOpenInProject(
765 '_undefined_status_', config))
766
767 # ensure closed means closed
768 self.assertFalse(tracker_helpers.MeansOpenInProject('Old', config))
769 self.assertFalse(tracker_helpers.MeansOpenInProject('old', config))
770 self.assertFalse(tracker_helpers.MeansOpenInProject(
771 'StatusThatWeDontUseAnymore', config))
772
773 def testIsNoisy(self):
774 self.assertTrue(tracker_helpers.IsNoisy(778, 320))
775 self.assertFalse(tracker_helpers.IsNoisy(20, 500))
776 self.assertFalse(tracker_helpers.IsNoisy(500, 20))
777 self.assertFalse(tracker_helpers.IsNoisy(1, 1))
778
779 def testMergeCCsAndAddComment(self):
780 target_issue = fake.MakeTestIssue(
781 789, 10, 'Target issue', 'New', 111)
782 source_issue = fake.MakeTestIssue(
783 789, 100, 'Source issue', 'New', 222)
784 source_issue.cc_ids.append(111)
785 # Issue without owner
786 source_issue_2 = fake.MakeTestIssue(
787 789, 101, 'Source issue 2', 'New', 0)
788
789 self.services.issue.TestAddIssue(target_issue)
790 self.services.issue.TestAddIssue(source_issue)
791 self.services.issue.TestAddIssue(source_issue_2)
792
793 # We copy this list so that it isn't updated by the test framework
794 initial_issue_comments = (
795 self.services.issue.GetCommentsForIssue(
796 'fake cnxn', target_issue.issue_id)[:])
797 mr = testing_helpers.MakeMonorailRequest(user_info={'user_id': 111})
798
799 # Merging source into target should create a comment.
800 self.assertIsNotNone(
801 tracker_helpers.MergeCCsAndAddComment(
802 self.services, mr, source_issue, target_issue))
803 updated_issue_comments = self.services.issue.GetCommentsForIssue(
804 'fake cnxn', target_issue.issue_id)
805 for comment in initial_issue_comments:
806 self.assertIn(comment, updated_issue_comments)
807 self.assertEqual(
808 len(initial_issue_comments) + 1, len(updated_issue_comments))
809
810 # Merging source into target should add source's owner to target's CCs.
811 updated_target_issue = self.services.issue.GetIssueByLocalID(
812 'fake cnxn', 789, 10)
813 self.assertIn(111, updated_target_issue.cc_ids)
814 self.assertIn(222, updated_target_issue.cc_ids)
815
816 # Merging source 2 into target should make a comment, but not update CCs.
817 self.assertIsNotNone(
818 tracker_helpers.MergeCCsAndAddComment(
819 self.services, mr, source_issue_2, updated_target_issue))
820 updated_target_issue = self.services.issue.GetIssueByLocalID(
821 'fake cnxn', 789, 10)
822 self.assertNotIn(0, updated_target_issue.cc_ids)
823
824 def testMergeCCsAndAddComment_RestrictedSourceIssue(self):
825 target_issue = fake.MakeTestIssue(
826 789, 10, 'Target issue', 'New', 222)
827 target_issue_2 = fake.MakeTestIssue(
828 789, 11, 'Target issue 2', 'New', 222)
829 source_issue = fake.MakeTestIssue(
830 789, 100, 'Source issue', 'New', 111)
831 source_issue.cc_ids.append(111)
832 source_issue.labels.append('Restrict-View-Commit')
833 target_issue_2.labels.append('Restrict-View-Commit')
834
835 self.services.issue.TestAddIssue(source_issue)
836 self.services.issue.TestAddIssue(target_issue)
837 self.services.issue.TestAddIssue(target_issue_2)
838
839 # We copy this list so that it isn't updated by the test framework
840 initial_issue_comments = self.services.issue.GetCommentsForIssue(
841 'fake cnxn', target_issue.issue_id)[:]
842 mr = testing_helpers.MakeMonorailRequest(user_info={'user_id': 111})
843 self.assertIsNotNone(
844 tracker_helpers.MergeCCsAndAddComment(
845 self.services, mr, source_issue, target_issue))
846
847 # When the source is restricted, we update the target comments...
848 updated_issue_comments = self.services.issue.GetCommentsForIssue(
849 'fake cnxn', target_issue.issue_id)
850 for comment in initial_issue_comments:
851 self.assertIn(comment, updated_issue_comments)
852 self.assertEqual(
853 len(initial_issue_comments) + 1, len(updated_issue_comments))
854 # ...but not the target CCs...
855 updated_target_issue = self.services.issue.GetIssueByLocalID(
856 'fake cnxn', 789, 10)
857 self.assertNotIn(111, updated_target_issue.cc_ids)
858 # ...unless both issues have the same restrictions.
859 self.assertIsNotNone(
860 tracker_helpers.MergeCCsAndAddComment(
861 self.services, mr, source_issue, target_issue_2))
862 updated_target_issue_2 = self.services.issue.GetIssueByLocalID(
863 'fake cnxn', 789, 11)
864 self.assertIn(111, updated_target_issue_2.cc_ids)
865
866 def testMergeCCsAndAddCommentMultipleIssues(self):
867 pass # TODO(jrobbins): Write this test.
868
869 def testGetAttachmentIfAllowed(self):
870 pass # TODO(jrobbins): Write this test.
871
872 def testLabelsMaskedByFields(self):
873 pass # TODO(jrobbins): Write this test.
874
875 def testLabelsNotMaskedByFields(self):
876 pass # TODO(jrobbins): Write this test.
877
878 def testLookupComponentIDs(self):
879 pass # TODO(jrobbins): Write this test.
880
881 def testParsePostDataUsers(self):
882 pd_users = 'a@example.com, b@example.com'
883
884 pd_users_ids, pd_users_str = tracker_helpers.ParsePostDataUsers(
885 self.cnxn, pd_users, self.services.user)
886
887 self.assertEqual([1, 2], sorted(pd_users_ids))
888 self.assertEqual('a@example.com, b@example.com', pd_users_str)
889
890 def testParsePostDataUsers_Empty(self):
891 pd_users = ''
892
893 pd_users_ids, pd_users_str = tracker_helpers.ParsePostDataUsers(
894 self.cnxn, pd_users, self.services.user)
895
896 self.assertEqual([], sorted(pd_users_ids))
897 self.assertEqual('', pd_users_str)
898
899 def testFilterIssueTypes(self):
900 pass # TODO(jrobbins): Write this test.
901
902 # ParseMergeFields is tested in IssueMergeTest.
903 # AddIssueStarrers is tested in IssueMergeTest.testMergeIssueStars().
904 # IsMergeAllowed is tested in IssueMergeTest.
905
906 def testPairDerivedValuesWithRuleExplanations_Nothing(self):
907 """Test we return nothing for an issue with no derived values."""
908 proposed_issue = tracker_pb2.Issue() # No derived values.
909 traces = {}
910 derived_users_by_id = {}
911 actual = tracker_helpers.PairDerivedValuesWithRuleExplanations(
912 proposed_issue, traces, derived_users_by_id)
913 (derived_labels_and_why, derived_owner_and_why,
914 derived_cc_and_why, warnings_and_why, errors_and_why) = actual
915 self.assertEqual([], derived_labels_and_why)
916 self.assertEqual([], derived_owner_and_why)
917 self.assertEqual([], derived_cc_and_why)
918 self.assertEqual([], warnings_and_why)
919 self.assertEqual([], errors_and_why)
920
921 def testPairDerivedValuesWithRuleExplanations_SomeValues(self):
922 """Test we return derived values and explanations for an issue."""
923 proposed_issue = tracker_pb2.Issue(
924 derived_owner_id=111, derived_cc_ids=[222, 333],
925 derived_labels=['aaa', 'zzz'],
926 derived_warnings=['Watch out'],
927 derived_errors=['Status Assigned requires an owner'])
928 traces = {
929 (tracker_pb2.FieldID.OWNER, 111): 'explain 1',
930 (tracker_pb2.FieldID.CC, 222): 'explain 2',
931 (tracker_pb2.FieldID.CC, 333): 'explain 3',
932 (tracker_pb2.FieldID.LABELS, 'aaa'): 'explain 4',
933 (tracker_pb2.FieldID.WARNING, 'Watch out'): 'explain 6',
934 (tracker_pb2.FieldID.ERROR,
935 'Status Assigned requires an owner'): 'explain 7',
936 # There can be extra traces that are not used.
937 (tracker_pb2.FieldID.LABELS, 'bbb'): 'explain 5',
938 # If there is no trace for some derived value, why is None.
939 }
940 derived_users_by_id = {
941 111: testing_helpers.Blank(display_name='one@example.com'),
942 222: testing_helpers.Blank(display_name='two@example.com'),
943 333: testing_helpers.Blank(display_name='three@example.com'),
944 }
945 actual = tracker_helpers.PairDerivedValuesWithRuleExplanations(
946 proposed_issue, traces, derived_users_by_id)
947 (derived_labels_and_why, derived_owner_and_why,
948 derived_cc_and_why, warnings_and_why, errors_and_why) = actual
949 self.assertEqual([
950 {'value': 'aaa', 'why': 'explain 4'},
951 {'value': 'zzz', 'why': None},
952 ], derived_labels_and_why)
953 self.assertEqual([
954 {'value': 'one@example.com', 'why': 'explain 1'},
955 ], derived_owner_and_why)
956 self.assertEqual([
957 {'value': 'two@example.com', 'why': 'explain 2'},
958 {'value': 'three@example.com', 'why': 'explain 3'},
959 ], derived_cc_and_why)
960 self.assertEqual([
961 {'value': 'Watch out', 'why': 'explain 6'},
962 ], warnings_and_why)
963 self.assertEqual([
964 {'value': 'Status Assigned requires an owner', 'why': 'explain 7'},
965 ], errors_and_why)
966
967
968class MakeViewsForUsersInIssuesTest(unittest.TestCase):
969
970 def setUp(self):
971 self.issue1 = _Issue('proj', 1)
972 self.issue1.owner_id = 1001
973 self.issue1.reporter_id = 1002
974
975 self.issue2 = _Issue('proj', 2)
976 self.issue2.owner_id = 2001
977 self.issue2.reporter_id = 2002
978 self.issue2.cc_ids.extend([1, 1001, 1002, 1003])
979
980 self.issue3 = _Issue('proj', 3)
981 self.issue3.owner_id = 1001
982 self.issue3.reporter_id = 3002
983
984 self.user = fake.UserService()
985 for user_id in [1, 1001, 1002, 1003, 2001, 2002, 3002]:
986 self.user.TestAddUser(
987 'test%d' % user_id, user_id, add_user=True)
988
989 def testMakeViewsForUsersInIssues(self):
990 issue_list = [self.issue1, self.issue2, self.issue3]
991 users_by_id = tracker_helpers.MakeViewsForUsersInIssues(
992 'fake cnxn', issue_list, self.user)
993 self.assertItemsEqual([0, 1, 1001, 1002, 1003, 2001, 2002, 3002],
994 list(users_by_id.keys()))
995 for user_id in [1001, 1002, 1003, 2001]:
996 self.assertEqual(users_by_id[user_id].user_id, user_id)
997
998 def testMakeViewsForUsersInIssuesOmittingSome(self):
999 issue_list = [self.issue1, self.issue2, self.issue3]
1000 users_by_id = tracker_helpers.MakeViewsForUsersInIssues(
1001 'fake cnxn', issue_list, self.user, omit_ids=[1001, 1003])
1002 self.assertItemsEqual([0, 1, 1002, 2001, 2002, 3002],
1003 list(users_by_id.keys()))
1004 for user_id in [1002, 2001, 2002, 3002]:
1005 self.assertEqual(users_by_id[user_id].user_id, user_id)
1006
1007 def testMakeViewsForUsersInIssuesEmpty(self):
1008 issue_list = []
1009 users_by_id = tracker_helpers.MakeViewsForUsersInIssues(
1010 'fake cnxn', issue_list, self.user)
1011 self.assertItemsEqual([], list(users_by_id.keys()))
1012
1013
1014class GetAllIssueProjectsTest(unittest.TestCase):
1015 issue_x_1 = tracker_pb2.Issue()
1016 issue_x_1.project_id = 789
1017 issue_x_1.local_id = 1
1018 issue_x_1.reporter_id = 1002
1019
1020 issue_x_2 = tracker_pb2.Issue()
1021 issue_x_2.project_id = 789
1022 issue_x_2.local_id = 2
1023 issue_x_2.reporter_id = 2002
1024
1025 issue_y_1 = tracker_pb2.Issue()
1026 issue_y_1.project_id = 678
1027 issue_y_1.local_id = 1
1028 issue_y_1.reporter_id = 2002
1029
1030 def setUp(self):
1031 self.project_service = fake.ProjectService()
1032 self.project_service.TestAddProject('proj-x', project_id=789)
1033 self.project_service.TestAddProject('proj-y', project_id=678)
1034 self.cnxn = 'fake connection'
1035
1036 def testGetAllIssueProjects_Empty(self):
1037 self.assertEqual(
1038 {}, tracker_helpers.GetAllIssueProjects(
1039 self.cnxn, [], self.project_service))
1040
1041 def testGetAllIssueProjects_Normal(self):
1042 self.assertEqual(
1043 {789: self.project_service.GetProjectByName(self.cnxn, 'proj-x')},
1044 tracker_helpers.GetAllIssueProjects(
1045 self.cnxn, [self.issue_x_1, self.issue_x_2], self.project_service))
1046 self.assertEqual(
1047 {789: self.project_service.GetProjectByName(self.cnxn, 'proj-x'),
1048 678: self.project_service.GetProjectByName(self.cnxn, 'proj-y')},
1049 tracker_helpers.GetAllIssueProjects(
1050 self.cnxn, [self.issue_x_1, self.issue_x_2, self.issue_y_1],
1051 self.project_service))
1052
1053
1054class FilterOutNonViewableIssuesTest(unittest.TestCase):
1055 owner_id = 111
1056 committer_id = 222
1057 nonmember_1_id = 1002
1058 nonmember_2_id = 2002
1059 nonmember_3_id = 3002
1060
1061 issue1 = tracker_pb2.Issue()
1062 issue1.project_name = 'proj'
1063 issue1.project_id = 789
1064 issue1.local_id = 1
1065 issue1.reporter_id = nonmember_1_id
1066
1067 issue2 = tracker_pb2.Issue()
1068 issue2.project_name = 'proj'
1069 issue2.project_id = 789
1070 issue2.local_id = 2
1071 issue2.reporter_id = nonmember_2_id
1072 issue2.labels.extend(['foo', 'bar'])
1073
1074 issue3 = tracker_pb2.Issue()
1075 issue3.project_name = 'proj'
1076 issue3.project_id = 789
1077 issue3.local_id = 3
1078 issue3.reporter_id = nonmember_3_id
1079 issue3.labels.extend(['restrict-view-commit'])
1080
1081 issue4 = tracker_pb2.Issue()
1082 issue4.project_name = 'proj'
1083 issue4.project_id = 789
1084 issue4.local_id = 4
1085 issue4.reporter_id = nonmember_3_id
1086 issue4.labels.extend(['Foo', 'Restrict-View-Commit'])
1087
1088 def setUp(self):
1089 self.user = user_pb2.User()
1090 self.project = self.MakeProject(project_pb2.ProjectState.LIVE)
1091 self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(
1092 self.project.project_id)
1093 self.project_dict = {self.project.project_id: self.project}
1094 self.config_dict = {self.config.project_id: self.config}
1095
1096 def MakeProject(self, state):
1097 p = project_pb2.Project(
1098 project_id=789, project_name='proj', state=state,
1099 owner_ids=[self.owner_id], committer_ids=[self.committer_id])
1100 return p
1101
1102 def testFilterOutNonViewableIssues_Member(self):
1103 # perms will be permissions.COMMITTER_ACTIVE_PERMISSIONSET
1104 filtered_issues = tracker_helpers.FilterOutNonViewableIssues(
1105 {self.committer_id}, self.user, self.project_dict,
1106 self.config_dict,
1107 [self.issue1, self.issue2, self.issue3, self.issue4])
1108 self.assertListEqual([1, 2, 3, 4],
1109 [issue.local_id for issue in filtered_issues])
1110
1111 def testFilterOutNonViewableIssues_Owner(self):
1112 # perms will be permissions.OWNER_ACTIVE_PERMISSIONSET
1113 filtered_issues = tracker_helpers.FilterOutNonViewableIssues(
1114 {self.owner_id}, self.user, self.project_dict, self.config_dict,
1115 [self.issue1, self.issue2, self.issue3, self.issue4])
1116 self.assertListEqual([1, 2, 3, 4],
1117 [issue.local_id for issue in filtered_issues])
1118
1119 def testFilterOutNonViewableIssues_Empty(self):
1120 # perms will be permissions.COMMITTER_ACTIVE_PERMISSIONSET
1121 filtered_issues = tracker_helpers.FilterOutNonViewableIssues(
1122 {self.committer_id}, self.user, self.project_dict,
1123 self.config_dict, [])
1124 self.assertListEqual([], filtered_issues)
1125
1126 def testFilterOutNonViewableIssues_NonMember(self):
1127 # perms will be permissions.READ_ONLY_PERMISSIONSET
1128 filtered_issues = tracker_helpers.FilterOutNonViewableIssues(
1129 {self.nonmember_1_id}, self.user, self.project_dict,
1130 self.config_dict, [self.issue1, self.issue2, self.issue3, self.issue4])
1131 self.assertListEqual([1, 2],
1132 [issue.local_id for issue in filtered_issues])
1133
1134 def testFilterOutNonViewableIssues_Reporter(self):
1135 # perms will be permissions.READ_ONLY_PERMISSIONSET
1136 filtered_issues = tracker_helpers.FilterOutNonViewableIssues(
1137 {self.nonmember_3_id}, self.user, self.project_dict,
1138 self.config_dict, [self.issue1, self.issue2, self.issue3, self.issue4])
1139 self.assertListEqual([1, 2, 3, 4],
1140 [issue.local_id for issue in filtered_issues])
1141
1142
1143class IssueMergeTest(unittest.TestCase):
1144
1145 def setUp(self):
1146 self.cnxn = 'fake cnxn'
1147 self.services = service_manager.Services(
1148 config=fake.ConfigService(),
1149 issue=fake.IssueService(),
1150 user=fake.UserService(),
1151 project=fake.ProjectService(),
1152 issue_star=fake.IssueStarService(),
1153 spam=fake.SpamService()
1154 )
1155 self.project = self.services.project.TestAddProject('proj', project_id=987)
1156 self.config = tracker_bizobj.MakeDefaultProjectIssueConfig(
1157 self.project.project_id)
1158 self.project_dict = {self.project.project_id: self.project}
1159 self.config_dict = {self.config.project_id: self.config}
1160
1161 def testParseMergeFields_NotSpecified(self):
1162 issue = fake.MakeTestIssue(987, 1, 'summary', 'New', 111)
1163 errors = template_helpers.EZTError()
1164 post_data = {}
1165
1166 text, merge_into_issue = tracker_helpers.ParseMergeFields(
1167 self.cnxn, None, 'proj', post_data, 'New', self.config, issue, errors)
1168 self.assertEqual('', text)
1169 self.assertEqual(None, merge_into_issue)
1170
1171 text, merge_into_issue = tracker_helpers.ParseMergeFields(
1172 self.cnxn, None, 'proj', post_data, 'Duplicate', self.config, issue,
1173 errors)
1174 self.assertEqual('', text)
1175 self.assertTrue(errors.merge_into_id)
1176 self.assertEqual(None, merge_into_issue)
1177
1178 def testParseMergeFields_WrongStatus(self):
1179 issue = fake.MakeTestIssue(987, 1, 'summary', 'New', 111)
1180 errors = template_helpers.EZTError()
1181 post_data = {'merge_into': '12'}
1182
1183 text, merge_into_issue = tracker_helpers.ParseMergeFields(
1184 self.cnxn, None, 'proj', post_data, 'New', self.config, issue, errors)
1185 self.assertEqual('', text)
1186 self.assertEqual(None, merge_into_issue)
1187
1188 def testParseMergeFields_NoSuchIssue(self):
1189 issue = fake.MakeTestIssue(987, 1, 'summary', 'New', 111)
1190 issue.merged_into = 12
1191 errors = template_helpers.EZTError()
1192 post_data = {'merge_into': '12'}
1193
1194 text, merge_into_issue = tracker_helpers.ParseMergeFields(
1195 self.cnxn, self.services, 'proj', post_data, 'Duplicate',
1196 self.config, issue, errors)
1197 self.assertEqual('12', text)
1198 self.assertEqual(None, merge_into_issue)
1199
1200 def testParseMergeFields_DontSelfMerge(self):
1201 issue = fake.MakeTestIssue(987, 1, 'summary', 'New', 111)
1202 errors = template_helpers.EZTError()
1203 post_data = {'merge_into': '1'}
1204
1205 text, merge_into_issue = tracker_helpers.ParseMergeFields(
1206 self.cnxn, self.services, 'proj', post_data, 'Duplicate', self.config,
1207 issue, errors)
1208 self.assertEqual('1', text)
1209 self.assertEqual(None, merge_into_issue)
1210 self.assertEqual('Cannot merge issue into itself', errors.merge_into_id)
1211
1212 def testParseMergeFields_NewIssueToMerge(self):
1213 merged_issue = fake.MakeTestIssue(
1214 self.project.project_id,
1215 1,
1216 'unused_summary',
1217 'unused_status',
1218 111,
1219 reporter_id=111)
1220 self.services.issue.TestAddIssue(merged_issue)
1221 mergee_issue = fake.MakeTestIssue(
1222 self.project.project_id,
1223 2,
1224 'unused_summary',
1225 'unused_status',
1226 111,
1227 reporter_id=111)
1228 self.services.issue.TestAddIssue(mergee_issue)
1229
1230 errors = template_helpers.EZTError()
1231 post_data = {'merge_into': str(mergee_issue.local_id)}
1232
1233 text, merge_into_issue = tracker_helpers.ParseMergeFields(
1234 self.cnxn, self.services, 'proj', post_data, 'Duplicate', self.config,
1235 merged_issue, errors)
1236 self.assertEqual(str(mergee_issue.local_id), text)
1237 self.assertEqual(mergee_issue, merge_into_issue)
1238
1239 def testIsMergeAllowed(self):
1240 mr = testing_helpers.MakeMonorailRequest()
1241 issue = fake.MakeTestIssue(987, 1, 'summary', 'New', 111)
1242 issue.project_name = self.project.project_name
1243
1244 for (perm_set, expected_merge_allowed) in (
1245 (permissions.READ_ONLY_PERMISSIONSET, False),
1246 (permissions.COMMITTER_INACTIVE_PERMISSIONSET, False),
1247 (permissions.COMMITTER_ACTIVE_PERMISSIONSET, True),
1248 (permissions.OWNER_ACTIVE_PERMISSIONSET, True)):
1249 mr.perms = perm_set
1250 merge_allowed = tracker_helpers.IsMergeAllowed(issue, mr, self.services)
1251 self.assertEqual(expected_merge_allowed, merge_allowed)
1252
1253 def testMergeIssueStars(self):
1254 mr = testing_helpers.MakeMonorailRequest()
1255 mr.project_name = self.project.project_name
1256 mr.project = self.project
1257
1258 config = self.services.config.GetProjectConfig(
1259 self.cnxn, self.project.project_id)
1260 self.services.issue_star.SetStar(
1261 self.cnxn, self.services, config, 1, 1, True)
1262 self.services.issue_star.SetStar(
1263 self.cnxn, self.services, config, 1, 2, True)
1264 self.services.issue_star.SetStar(
1265 self.cnxn, self.services, config, 1, 3, True)
1266 self.services.issue_star.SetStar(
1267 self.cnxn, self.services, config, 3, 3, True)
1268 self.services.issue_star.SetStar(
1269 self.cnxn, self.services, config, 3, 6, True)
1270 self.services.issue_star.SetStar(
1271 self.cnxn, self.services, config, 2, 3, True)
1272 self.services.issue_star.SetStar(
1273 self.cnxn, self.services, config, 2, 4, True)
1274 self.services.issue_star.SetStar(
1275 self.cnxn, self.services, config, 2, 5, True)
1276
1277 new_starrers = tracker_helpers.GetNewIssueStarrers(
1278 self.cnxn, self.services, [1, 3], 2)
1279 self.assertItemsEqual(new_starrers, [1, 2, 6])
1280 tracker_helpers.AddIssueStarrers(
1281 self.cnxn, self.services, mr, 2, self.project, new_starrers)
1282 issue_2_starrers = self.services.issue_star.LookupItemStarrers(
1283 self.cnxn, 2)
1284 # XXX(jrobbins): these tests incorrectly mix local IDs with IIDs.
1285 self.assertItemsEqual([1, 2, 3, 4, 5, 6], issue_2_starrers)
1286
1287
1288class MergeLinkedMembersTest(unittest.TestCase):
1289
1290 def setUp(self):
1291 self.cnxn = 'fake cnxn'
1292 self.services = service_manager.Services(
1293 user=fake.UserService())
1294 self.user1 = self.services.user.TestAddUser('one@example.com', 111)
1295 self.user2 = self.services.user.TestAddUser('two@example.com', 222)
1296
1297 def testNoLinkedAccounts(self):
1298 """When no candidate accounts are linked, they are all returned."""
1299 actual = tracker_helpers._MergeLinkedMembers(
1300 self.cnxn, self.services.user, [111, 222])
1301 self.assertEqual([111, 222], actual)
1302
1303 def testSomeLinkedButNoMasking(self):
1304 """If an account has linked accounts, but they are not here, keep it."""
1305 self.user1.linked_child_ids = [999]
1306 self.user2.linked_parent_id = 999
1307 actual = tracker_helpers._MergeLinkedMembers(
1308 self.cnxn, self.services.user, [111, 222])
1309 self.assertEqual([111, 222], actual)
1310
1311 def testParentMasksChild(self):
1312 """When two accounts linked, only the parent is returned."""
1313 self.user2.linked_parent_id = 111
1314 actual = tracker_helpers._MergeLinkedMembers(
1315 self.cnxn, self.services.user, [111, 222])
1316 self.assertEqual([111], actual)
1317
1318
1319class FilterMemberDataTest(unittest.TestCase):
1320
1321 def setUp(self):
1322 services = service_manager.Services(
1323 project=fake.ProjectService(),
1324 config=fake.ConfigService(),
1325 issue=fake.IssueService(),
1326 user=fake.UserService())
1327 self.owner_email = 'owner@dom.com'
1328 self.committer_email = 'commit@dom.com'
1329 self.contributor_email = 'contrib@dom.com'
1330 self.indirect_member_email = 'ind@dom.com'
1331 self.all_emails = [self.owner_email, self.committer_email,
1332 self.contributor_email, self.indirect_member_email]
1333 self.project = services.project.TestAddProject('proj')
1334
1335 def DoFiltering(self, perms, unsigned_user=False):
1336 mr = testing_helpers.MakeMonorailRequest(
1337 project=self.project, perms=perms)
1338 if not unsigned_user:
1339 mr.auth.user_id = 111
1340 mr.auth.user_view = testing_helpers.Blank(domain='jrobbins.org')
1341 return tracker_helpers._FilterMemberData(
1342 mr, [self.owner_email], [self.committer_email],
1343 [self.contributor_email], [self.indirect_member_email], mr.project)
1344
1345 def testUnsignedUser_NormalProject(self):
1346 visible_members = self.DoFiltering(
1347 permissions.READ_ONLY_PERMISSIONSET, unsigned_user=True)
1348 self.assertItemsEqual(
1349 [self.owner_email, self.committer_email, self.contributor_email,
1350 self.indirect_member_email],
1351 visible_members)
1352
1353 def testUnsignedUser_RestrictedProject(self):
1354 self.project.only_owners_see_contributors = True
1355 visible_members = self.DoFiltering(
1356 permissions.READ_ONLY_PERMISSIONSET, unsigned_user=True)
1357 self.assertItemsEqual(
1358 [self.owner_email, self.committer_email, self.indirect_member_email],
1359 visible_members)
1360
1361 def testOwnersAndAdminsCanSeeAll_NormalProject(self):
1362 visible_members = self.DoFiltering(
1363 permissions.OWNER_ACTIVE_PERMISSIONSET)
1364 self.assertItemsEqual(self.all_emails, visible_members)
1365
1366 visible_members = self.DoFiltering(
1367 permissions.ADMIN_PERMISSIONSET)
1368 self.assertItemsEqual(self.all_emails, visible_members)
1369
1370 def testOwnersAndAdminsCanSeeAll_HubAndSpoke(self):
1371 self.project.only_owners_see_contributors = True
1372
1373 visible_members = self.DoFiltering(
1374 permissions.OWNER_ACTIVE_PERMISSIONSET)
1375 self.assertItemsEqual(self.all_emails, visible_members)
1376
1377 visible_members = self.DoFiltering(
1378 permissions.ADMIN_PERMISSIONSET)
1379 self.assertItemsEqual(self.all_emails, visible_members)
1380
1381 visible_members = self.DoFiltering(
1382 permissions.COMMITTER_ACTIVE_PERMISSIONSET)
1383 self.assertItemsEqual(self.all_emails, visible_members)
1384
1385 def testNonOwnersCanSeeAll_NormalProject(self):
1386 visible_members = self.DoFiltering(
1387 permissions.COMMITTER_ACTIVE_PERMISSIONSET)
1388 self.assertItemsEqual(self.all_emails, visible_members)
1389
1390 visible_members = self.DoFiltering(
1391 permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET)
1392 self.assertItemsEqual(self.all_emails, visible_members)
1393
1394 def testCommittersSeeOnlySameDomain_HubAndSpoke(self):
1395 self.project.only_owners_see_contributors = True
1396
1397 visible_members = self.DoFiltering(
1398 permissions.CONTRIBUTOR_ACTIVE_PERMISSIONSET)
1399 self.assertItemsEqual(
1400 [self.owner_email, self.committer_email, self.indirect_member_email],
1401 visible_members)
1402
1403
1404class GetLabelOptionsTest(unittest.TestCase):
1405
1406 @mock.patch('tracker.tracker_helpers.LabelsNotMaskedByFields')
1407 def testGetLabelOptions(self, mockLabelsNotMaskedByFields):
1408 mockLabelsNotMaskedByFields.return_value = []
1409 config = tracker_pb2.ProjectIssueConfig()
1410 custom_perms = []
1411 actual = tracker_helpers.GetLabelOptions(config, custom_perms)
1412 expected = [
1413 {'doc': 'Only users who can edit the issue may access it',
1414 'name': 'Restrict-View-EditIssue'},
1415 {'doc': 'Only users who can edit the issue may add comments',
1416 'name': 'Restrict-AddIssueComment-EditIssue'},
1417 {'doc': 'Custom permission CoreTeam is needed to access',
1418 'name': 'Restrict-View-CoreTeam'}
1419 ]
1420 self.assertEqual(expected, actual)
1421
1422 def testBuildRestrictionChoices(self):
1423 choices = tracker_helpers._BuildRestrictionChoices([], [], [])
1424 self.assertEqual([], choices)
1425
1426 choices = tracker_helpers._BuildRestrictionChoices(
1427 [], ['Hop', 'Jump'], [])
1428 self.assertEqual([], choices)
1429
1430 freq = [('View', 'B', 'You need permission B to do anything'),
1431 ('A', 'B', 'You need B to use A')]
1432 choices = tracker_helpers._BuildRestrictionChoices(freq, [], [])
1433 expected = [dict(name='Restrict-View-B',
1434 doc='You need permission B to do anything'),
1435 dict(name='Restrict-A-B',
1436 doc='You need B to use A')]
1437 self.assertListEqual(expected, choices)
1438
1439 extra_perms = ['Over18', 'Over21']
1440 choices = tracker_helpers._BuildRestrictionChoices(
1441 [], ['Drink', 'Smoke'], extra_perms)
1442 expected = [dict(name='Restrict-Drink-Over18',
1443 doc='Permission Over18 needed to use Drink'),
1444 dict(name='Restrict-Drink-Over21',
1445 doc='Permission Over21 needed to use Drink'),
1446 dict(name='Restrict-Smoke-Over18',
1447 doc='Permission Over18 needed to use Smoke'),
1448 dict(name='Restrict-Smoke-Over21',
1449 doc='Permission Over21 needed to use Smoke')]
1450 self.assertListEqual(expected, choices)
1451
1452
1453class FilterKeptAttachmentsTest(unittest.TestCase):
1454 def testFilterKeptAttachments(self):
1455 comments = [
1456 tracker_pb2.IssueComment(
1457 is_description=True,
1458 attachments=[tracker_pb2.Attachment(attachment_id=1)]),
1459 tracker_pb2.IssueComment(),
1460 tracker_pb2.IssueComment(
1461 is_description=True,
1462 attachments=[
1463 tracker_pb2.Attachment(attachment_id=2),
1464 tracker_pb2.Attachment(attachment_id=3)]),
1465 tracker_pb2.IssueComment(),
1466 tracker_pb2.IssueComment(
1467 approval_id=24,
1468 is_description=True,
1469 attachments=[tracker_pb2.Attachment(attachment_id=4)])]
1470
1471 filtered = tracker_helpers.FilterKeptAttachments(
1472 True, [1, 2, 3, 4], comments, None)
1473 self.assertEqual([2, 3], filtered)
1474
1475 def testApprovalDescription(self):
1476 comments = [
1477 tracker_pb2.IssueComment(
1478 is_description=True,
1479 attachments=[tracker_pb2.Attachment(attachment_id=1)]),
1480 tracker_pb2.IssueComment(),
1481 tracker_pb2.IssueComment(
1482 is_description=True,
1483 attachments=[
1484 tracker_pb2.Attachment(attachment_id=2),
1485 tracker_pb2.Attachment(attachment_id=3)]),
1486 tracker_pb2.IssueComment(),
1487 tracker_pb2.IssueComment(
1488 approval_id=24,
1489 is_description=True,
1490 attachments=[tracker_pb2.Attachment(attachment_id=4)])]
1491
1492 filtered = tracker_helpers.FilterKeptAttachments(
1493 True, [1, 2, 3, 4], comments, 24)
1494 self.assertEqual([4], filtered)
1495
1496 def testNotAnIssueDescription(self):
1497 comments = [
1498 tracker_pb2.IssueComment(
1499 is_description=True,
1500 attachments=[tracker_pb2.Attachment(attachment_id=1)]),
1501 tracker_pb2.IssueComment(),
1502 tracker_pb2.IssueComment(
1503 is_description=True,
1504 attachments=[
1505 tracker_pb2.Attachment(attachment_id=2),
1506 tracker_pb2.Attachment(attachment_id=3)]),
1507 tracker_pb2.IssueComment(),
1508 tracker_pb2.IssueComment(
1509 approval_id=24,
1510 is_description=True,
1511 attachments=[tracker_pb2.Attachment(attachment_id=4)])]
1512
1513 filtered = tracker_helpers.FilterKeptAttachments(
1514 False, [1, 2, 3, 4], comments, None)
1515 self.assertIsNone(filtered)
1516
1517 def testNoDescriptionsInComments(self):
1518 comments = [
1519 tracker_pb2.IssueComment(),
1520 tracker_pb2.IssueComment()]
1521
1522 filtered = tracker_helpers.FilterKeptAttachments(
1523 True, [1, 2, 3, 4], comments, None)
1524 self.assertEqual([], filtered)
1525
1526 def testNoComments(self):
1527 filtered = tracker_helpers.FilterKeptAttachments(
1528 True, [1, 2, 3, 4], [], None)
1529 self.assertEqual([], filtered)
1530
1531
1532class EnumFieldHelpersTest(unittest.TestCase):
1533
1534 def test_GetEnumFieldValuesAndDocstrings(self):
1535 """We can get all choices for an enum field"""
1536 fd = tracker_pb2.FieldDef(
1537 field_id=123,
1538 project_id=1,
1539 field_name='yellow',
1540 field_type=tracker_pb2.FieldTypes.ENUM_TYPE)
1541 ld_1 = tracker_pb2.LabelDef(
1542 label='yellow-submarine', label_docstring='ld_1_docstring')
1543 ld_2 = tracker_pb2.LabelDef(
1544 label='yellow-tisket', label_docstring='ld_2_docstring')
1545 ld_3 = tracker_pb2.LabelDef(
1546 label='yellow-basket', label_docstring='ld_3_docstring')
1547 ld_4 = tracker_pb2.LabelDef(
1548 label='yellow', label_docstring='ld_4_docstring')
1549 ld_5 = tracker_pb2.LabelDef(
1550 label='not-yellow', label_docstring='ld_5_docstring')
1551 ld_6 = tracker_pb2.LabelDef(
1552 label='yellow-tasket',
1553 label_docstring='ld_6_docstring',
1554 deprecated=True)
1555 config = tracker_pb2.ProjectIssueConfig(
1556 default_template_for_developers=1,
1557 default_template_for_users=2,
1558 well_known_labels=[ld_1, ld_2, ld_3, ld_4, ld_5, ld_6])
1559 actual = tracker_helpers._GetEnumFieldValuesAndDocstrings(fd, config)
1560 # Expect to omit labels `yellow` and `not-yellow` due to prefix mismatch
1561 # Also expect to omit label `yellow-tasket` because it's deprecated
1562 expected = [
1563 ('submarine', 'ld_1_docstring'), ('tisket', 'ld_2_docstring'),
1564 ('basket', 'ld_3_docstring')
1565 ]
1566 self.assertEqual(expected, actual)
1567
1568
1569class CreateIssueHelpersTest(unittest.TestCase):
1570
1571 def setUp(self):
1572 self.services = service_manager.Services(
1573 project=fake.ProjectService(),
1574 config=fake.ConfigService(),
1575 issue=fake.IssueService(),
1576 user=fake.UserService(),
1577 usergroup=fake.UserGroupService())
1578 self.cnxn = 'fake cnxn'
1579
1580 self.project_member = self.services.user.TestAddUser(
1581 'user_1@example.com', 111)
1582 self.project_group_member = self.services.user.TestAddUser(
1583 'group@example.com', 999)
1584 self.project = self.services.project.TestAddProject(
1585 'proj',
1586 project_id=789,
1587 committer_ids=[
1588 self.project_member.user_id, self.project_group_member.user_id
1589 ])
1590 self.no_project_user = self.services.user.TestAddUser(
1591 'user_2@example.com', 222)
1592 self.config = fake.MakeTestConfig(self.project.project_id, [], [])
1593 self.int_fd = tracker_bizobj.MakeFieldDef(
1594 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None, '', False,
1595 False, False, None, None, '', False, '', '',
1596 tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
1597 self.int_fd.max_value = 999
1598 self.config.field_defs = [self.int_fd]
1599 self.status_1 = tracker_pb2.StatusDef(
1600 status='New', means_open=True, status_docstring='status_1 docstring')
1601 self.config.well_known_statuses = [self.status_1]
1602 self.component_def_1 = tracker_pb2.ComponentDef(
1603 component_id=1, path='compFOO')
1604 self.component_def_2 = tracker_pb2.ComponentDef(
1605 component_id=2, path='deprecated', deprecated=True)
1606 self.config.component_defs = [self.component_def_1, self.component_def_2]
1607 self.services.config.StoreConfig('cnxn', self.config)
1608 self.services.usergroup.TestAddGroupSettings(999, 'group@example.com')
1609
1610 def testAssertValidIssueForCreate_Valid(self):
1611 input_issue = tracker_pb2.Issue(
1612 summary='sum',
1613 status='New',
1614 owner_id=111,
1615 project_id=789,
1616 component_ids=[1],
1617 cc_ids=[999])
1618 tracker_helpers.AssertValidIssueForCreate(
1619 self.cnxn, self.services, input_issue, 'nonempty description')
1620
1621 def testAssertValidIssueForCreate_ValidatesOwner(self):
1622 input_issue = tracker_pb2.Issue(
1623 summary='sum', status='New', owner_id=222, project_id=789)
1624 with self.assertRaisesRegexp(exceptions.InputException,
1625 'Issue owner must be a project member'):
1626 tracker_helpers.AssertValidIssueForCreate(
1627 self.cnxn, self.services, input_issue, 'nonempty description')
1628 input_issue.owner_id = 333
1629 with self.assertRaisesRegexp(exceptions.InputException,
1630 'Issue owner user ID not found'):
1631 tracker_helpers.AssertValidIssueForCreate(
1632 self.cnxn, self.services, input_issue, 'nonempty description')
1633 input_issue.owner_id = 999
1634 with self.assertRaisesRegexp(exceptions.InputException,
1635 'Issue owner cannot be a user group'):
1636 tracker_helpers.AssertValidIssueForCreate(
1637 self.cnxn, self.services, input_issue, 'nonempty description')
1638
1639 def testAssertValidIssueForCreate_ValidatesSummary(self):
1640 input_issue = tracker_pb2.Issue(
1641 summary='', status='New', owner_id=111, project_id=789)
1642 with self.assertRaisesRegexp(exceptions.InputException,
1643 'Summary is required'):
1644 tracker_helpers.AssertValidIssueForCreate(
1645 self.cnxn, self.services, input_issue, 'nonempty description')
1646 input_issue.summary = ' '
1647 tracker_helpers.AssertValidIssueForCreate(
1648 self.cnxn, self.services, input_issue, 'nonempty description')
1649
1650 def testAssertValidIssueForCreate_ValidatesDescription(self):
1651 input_issue = tracker_pb2.Issue(
1652 summary='sum', status='New', owner_id=111, project_id=789)
1653 with self.assertRaisesRegexp(exceptions.InputException,
1654 'Description is required'):
1655 tracker_helpers.AssertValidIssueForCreate(
1656 self.cnxn, self.services, input_issue, '')
1657 tracker_helpers.AssertValidIssueForCreate(
1658 self.cnxn, self.services, input_issue, ' ')
1659
1660 def testAssertValidIssueForCreate_ValidatesFieldDef(self):
1661 fv = tracker_bizobj.MakeFieldValue(
1662 self.int_fd.field_id, 1000, None, None, None, None, False)
1663 input_issue = tracker_pb2.Issue(
1664 summary='sum',
1665 status='New',
1666 owner_id=111,
1667 project_id=789,
1668 field_values=[fv])
1669 with self.assertRaises(exceptions.InputException):
1670 tracker_helpers.AssertValidIssueForCreate(
1671 self.cnxn, self.services, input_issue, 'nonempty description')
1672
1673 def testAssertValidIssueForCreate_ValidatesStatus(self):
1674 input_issue = tracker_pb2.Issue(
1675 summary='sum', status='DNE_status', owner_id=111, project_id=789)
1676
1677 def mock_status_lookup(*_args, **_kwargs):
1678 return None
1679
1680 self.services.config.LookupStatusID = mock_status_lookup
1681 with self.assertRaisesRegexp(exceptions.InputException,
1682 'Undefined status: DNE_status'):
1683 tracker_helpers.AssertValidIssueForCreate(
1684 self.cnxn, self.services, input_issue, 'nonempty description')
1685
1686 def testAssertValidIssueForCreate_ValidatesComponents(self):
1687 # Tests an undefined component.
1688 input_issue = tracker_pb2.Issue(
1689 summary='',
1690 status='New',
1691 owner_id=111,
1692 project_id=789,
1693 component_ids=[3])
1694 with self.assertRaisesRegexp(
1695 exceptions.InputException,
1696 'Undefined or deprecated component with id: 3'):
1697 tracker_helpers.AssertValidIssueForCreate(
1698 self.cnxn, self.services, input_issue, 'nonempty description')
1699
1700 # Tests a deprecated component.
1701 input_issue = tracker_pb2.Issue(
1702 summary='',
1703 status='New',
1704 owner_id=111,
1705 project_id=789,
1706 component_ids=[self.component_def_2.component_id])
1707 with self.assertRaisesRegexp(
1708 exceptions.InputException,
1709 'Undefined or deprecated component with id: 2'):
1710 tracker_helpers.AssertValidIssueForCreate(
1711 self.cnxn, self.services, input_issue, 'nonempty description')
1712
1713 def testAssertValidIssueForCreate_ValidatesUsers(self):
1714 user_fd = tracker_bizobj.MakeFieldDef(
1715 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None, '', False,
1716 False, False, None, None, '', False, '', '',
1717 tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
1718 self.services.config.TestAddFieldDef(user_fd)
1719
1720 input_issue = tracker_pb2.Issue(
1721 summary='sum',
1722 status='New',
1723 owner_id=111,
1724 project_id=789,
1725 cc_ids=[123],
1726 field_values=[
1727 tracker_bizobj.MakeFieldValue(
1728 user_fd.field_id, None, None, 124, None, None, False)
1729 ])
1730 copied_issue = copy.deepcopy(input_issue)
1731 with self.assertRaisesRegexp(exceptions.InputException,
1732 r'users/123: .+\nusers/124: .+'):
1733 tracker_helpers.AssertValidIssueForCreate(
1734 self.cnxn, self.services, input_issue, 'nonempty description')
1735 self.assertEqual(input_issue, copied_issue)
1736
1737 self.services.user.TestAddUser('a@test.com', 123)
1738 self.services.user.TestAddUser('a@test.com', 124)
1739 tracker_helpers.AssertValidIssueForCreate(
1740 self.cnxn, self.services, input_issue, 'nonempty description')
1741 self.assertEqual(input_issue, copied_issue)
1742
1743
1744class ModifyIssuesHelpersTest(unittest.TestCase):
1745
1746 def setUp(self):
1747 self.services = service_manager.Services(
1748 project=fake.ProjectService(),
1749 config=fake.ConfigService(),
1750 issue=fake.IssueService(),
1751 issue_star=fake.IssueStarService(),
1752 user=fake.UserService(),
1753 usergroup=fake.UserGroupService())
1754 self.cnxn = 'fake cnxn'
1755
1756 self.project_member = self.services.user.TestAddUser(
1757 'user_1@example.com', 111)
1758 self.project = self.services.project.TestAddProject(
1759 'proj', project_id=789, committer_ids=[self.project_member.user_id])
1760 self.no_project_user = self.services.user.TestAddUser(
1761 'user_2@example.com', 222)
1762
1763 self.config = fake.MakeTestConfig(self.project.project_id, [], [])
1764 self.int_fd = tracker_bizobj.MakeFieldDef(
1765 123, 789, 'CPU', tracker_pb2.FieldTypes.INT_TYPE, None, '', False,
1766 False, False, None, None, '', False, '', '',
1767 tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
1768 self.int_fd.max_value = 999
1769 self.config.field_defs = [self.int_fd]
1770 self.services.config.StoreConfig('cnxn', self.config)
1771
1772 def testApplyAllIssueChanges(self):
1773 issue_delta_pairs = []
1774 no_change_iid = 78942
1775
1776 expected_issues_to_update = {}
1777 expected_amendments = {}
1778 expected_imp_amendments = {}
1779 expected_old_owners = {}
1780 expected_old_statuses = {}
1781 expected_old_components = {}
1782 expected_merged_from_add = {}
1783 expected_new_starrers = {}
1784
1785 issue_main = _Issue('proj', 100)
1786 issue_main_ref = ('proj', issue_main.local_id)
1787 issue_main.owner_id = 999
1788 issue_main.cc_ids = [111, 222]
1789 issue_main.labels = ['dont_touch', 'remove_me']
1790
1791 expected_main = copy.deepcopy(issue_main)
1792 expected_main.owner_id = 888
1793 expected_main.cc_ids = [111, 333]
1794 expected_main.labels = ['dont_touch', 'add_me']
1795 expected_amendments[issue_main.issue_id] = [
1796 tracker_bizobj.MakeOwnerAmendment(888, 999),
1797 tracker_bizobj.MakeCcAmendment([333], [222]),
1798 tracker_bizobj.MakeLabelsAmendment(['add_me'], ['remove_me'])
1799 ]
1800 expected_old_owners[issue_main.issue_id] = 999
1801
1802 # blocked_on issues changes setup.
1803 bo_add = _Issue('proj', 1)
1804 self.services.issue.TestAddIssue(bo_add)
1805 expected_bo_add = copy.deepcopy(bo_add)
1806 # All impacted issues should be fetched within ApplyAllIssueChanges
1807 # directly from the DB, skipping cache with `use_cache=False` in GetIssue().
1808 # So we expect these issues to have assume_stale=False.
1809 expected_bo_add.assume_stale = False
1810 expected_bo_add.blocking_iids = [issue_main.issue_id]
1811 expected_issues_to_update[expected_bo_add.issue_id] = expected_bo_add
1812 expected_imp_amendments[bo_add.issue_id] = [
1813 tracker_bizobj.MakeBlockingAmendment(
1814 [issue_main_ref], [], default_project_name='proj')
1815 ]
1816
1817 bo_remove = _Issue('proj', 2)
1818 bo_remove.blocking_iids = [issue_main.issue_id]
1819 self.services.issue.TestAddIssue(bo_remove)
1820 expected_bo_remove = copy.deepcopy(bo_remove)
1821 expected_bo_remove.assume_stale = False
1822 expected_bo_remove.blocking_iids = []
1823 expected_issues_to_update[expected_bo_remove.issue_id] = expected_bo_remove
1824 expected_imp_amendments[bo_remove.issue_id] = [
1825 tracker_bizobj.MakeBlockingAmendment(
1826 [], [issue_main_ref], default_project_name='proj')
1827 ]
1828
1829 issue_main.blocked_on_iids = [no_change_iid, bo_remove.issue_id]
1830 # By default new blocked_on issues that appear in blocked_on_iids
1831 # with no prior rank associated with it are un-ranked and assigned rank 0.
1832 # See SortBlockedOn in issue_svc.py.
1833 issue_main.blocked_on_ranks = [0, 0]
1834 expected_main.blocked_on_iids = [no_change_iid, bo_add.issue_id]
1835 expected_main.blocked_on_ranks = [0, 0]
1836 expected_amendments[issue_main.issue_id].append(
1837 tracker_bizobj.MakeBlockedOnAmendment(
1838 [('proj', bo_add.local_id)], [('proj', bo_remove.local_id)],
1839 default_project_name='proj'))
1840
1841 # blocking_issues changes setup.
1842 b_add = _Issue('proj', 3)
1843 self.services.issue.TestAddIssue(b_add)
1844 expected_b_add = copy.deepcopy(b_add)
1845 expected_b_add.assume_stale = False
1846 expected_b_add.blocked_on_iids = [issue_main.issue_id]
1847 expected_b_add.blocked_on_ranks = [0]
1848 expected_issues_to_update[expected_b_add.issue_id] = expected_b_add
1849 expected_imp_amendments[b_add.issue_id] = [
1850 tracker_bizobj.MakeBlockedOnAmendment(
1851 [issue_main_ref], [], default_project_name='proj')
1852 ]
1853
1854 b_remove = _Issue('proj', 4)
1855 b_remove.blocked_on_iids = [issue_main.issue_id]
1856 self.services.issue.TestAddIssue(b_remove)
1857 expected_b_remove = copy.deepcopy(b_remove)
1858 expected_b_remove.assume_stale = False
1859 expected_b_remove.blocked_on_iids = []
1860 # Test we can process delta changes and impact changes.
1861 delta_b_remove = tracker_pb2.IssueDelta(labels_add=['more_chickens'])
1862 expected_b_remove.labels = ['more_chickens']
1863 issue_delta_pairs.append((b_remove, delta_b_remove))
1864 expected_issues_to_update[expected_b_remove.issue_id] = expected_b_remove
1865 expected_imp_amendments[b_remove.issue_id] = [
1866 tracker_bizobj.MakeBlockedOnAmendment(
1867 [], [issue_main_ref], default_project_name='proj')
1868 ]
1869 expected_amendments[b_remove.issue_id] = [
1870 tracker_bizobj.MakeLabelsAmendment(['more_chickens'], [])
1871 ]
1872
1873 issue_main.blocking_iids = [no_change_iid, b_remove.issue_id]
1874 expected_main.blocking_iids = [no_change_iid, b_add.issue_id]
1875 expected_amendments[issue_main.issue_id].append(
1876 tracker_bizobj.MakeBlockingAmendment(
1877 [('proj', b_add.local_id)], [('proj', b_remove.local_id)],
1878 default_project_name='proj'))
1879
1880 # Merged issues changes setup.
1881 merge_remove = _Issue('proj', 5)
1882 self.services.issue.TestAddIssue(merge_remove)
1883 expected_merge_remove = copy.deepcopy(merge_remove)
1884 expected_merge_remove.assume_stale = False
1885 expected_issues_to_update[
1886 expected_merge_remove.issue_id] = expected_merge_remove
1887 expected_imp_amendments[merge_remove.issue_id] = [
1888 tracker_bizobj.MakeMergedIntoAmendment(
1889 [], [issue_main_ref], default_project_name='proj')
1890 ]
1891
1892 merge_add = _Issue('proj', 6)
1893 self.services.issue.TestAddIssue(merge_add)
1894 expected_merge_add = copy.deepcopy(merge_add)
1895 expected_merge_add.assume_stale = False
1896 # We are adding 333 and removing 222 in issue_main with delta_main.
1897 expected_merge_add.cc_ids = [expected_main.owner_id, 333, 111]
1898 expected_merged_from_add[expected_merge_add.issue_id] = [
1899 issue_main.issue_id
1900 ]
1901
1902 expected_imp_amendments[merge_add.issue_id] = [
1903 tracker_bizobj.MakeCcAmendment(expected_merge_add.cc_ids, []),
1904 tracker_bizobj.MakeMergedIntoAmendment(
1905 [issue_main_ref], [], default_project_name='proj')
1906 ]
1907 # We are merging issue_main into merge_add, so issue_main's starrers
1908 # should be merged into merge_add's starrers.
1909 self.services.issue_star.SetStar(
1910 self.cnxn, self.services, None, issue_main.issue_id, 111, True)
1911 self.services.issue_star.SetStar(
1912 self.cnxn, self.services, None, issue_main.issue_id, 222, True)
1913 expected_merge_add.star_count = 2
1914 expected_new_starrers[merge_add.issue_id] = [222, 111]
1915
1916 expected_issues_to_update[expected_merge_add.issue_id] = expected_merge_add
1917
1918
1919 issue_main.merged_into = merge_remove.issue_id
1920 expected_main.merged_into = merge_add.issue_id
1921 expected_amendments[issue_main.issue_id].append(
1922 tracker_bizobj.MakeMergedIntoAmendment(
1923 [('proj', merge_add.local_id)], [('proj', merge_remove.local_id)],
1924 default_project_name='proj'))
1925
1926 self.services.issue.TestAddIssue(issue_main)
1927 expected_issues_to_update[expected_main.issue_id] = expected_main
1928
1929
1930 # Issues we'll put in delta_main.*_remove fields that aren't in issue_main.
1931 # These issues should not show up in issues_to_update.
1932 missing_1 = _Issue('proj', 404)
1933 expected_missing_1 = copy.deepcopy(missing_1)
1934 expected_missing_1.assume_stale = False
1935 self.services.issue.TestAddIssue(missing_1)
1936 missing_2 = _Issue('proj', 405)
1937 self.services.issue.TestAddIssue(missing_2)
1938 expected_missing_2 = copy.deepcopy(missing_2)
1939 expected_missing_2.assume_stale = False
1940
1941 delta_main = tracker_pb2.IssueDelta(
1942 owner_id=888,
1943 cc_ids_remove=[222, 404], cc_ids_add=[333],
1944 labels_remove=['remove_me', 'remove_404'], labels_add=['add_me'],
1945 merged_into=merge_add.issue_id,
1946 blocked_on_add=[bo_add.issue_id],
1947 blocked_on_remove=[bo_remove.issue_id, missing_1.issue_id],
1948 blocking_add=[b_add.issue_id],
1949 blocking_remove=[b_remove.issue_id, missing_2.issue_id])
1950 issue_delta_pairs.append((issue_main, delta_main))
1951
1952 actual_tuple = tracker_helpers.ApplyAllIssueChanges(
1953 self.cnxn, issue_delta_pairs, self.services)
1954
1955 expected_tuple = tracker_helpers._IssueChangesTuple(
1956 expected_issues_to_update, expected_merged_from_add,
1957 expected_amendments, expected_imp_amendments, expected_old_owners,
1958 expected_old_statuses, expected_old_components, expected_new_starrers)
1959 self.assertEqual(actual_tuple, expected_tuple)
1960
1961 self.assertEqual(missing_1, expected_missing_1)
1962 self.assertEqual(missing_2, expected_missing_2)
1963
1964 def testApplyAllIssueChanges_NOOP(self):
1965 """Check we can ignore issue-delta pairs that are NOOP."""
1966 noop_issue = _Issue('proj', 1)
1967 bo_add_noop = _Issue('proj', 2)
1968 bo_remove_noop = _Issue('proj', 3)
1969
1970 noop_issue.owner_id = 111
1971 noop_issue.cc_ids = [222]
1972 noop_issue.blocked_on_iids = [bo_add_noop.issue_id]
1973 bo_add_noop.blocking_iids = [noop_issue.issue_id]
1974
1975 self.services.issue.TestAddIssue(noop_issue)
1976 self.services.issue.TestAddIssue(bo_add_noop)
1977 self.services.issue.TestAddIssue(bo_remove_noop)
1978 expected_noop_issue = copy.deepcopy(noop_issue)
1979 noop_delta = tracker_pb2.IssueDelta(
1980 owner_id=noop_issue.owner_id,
1981 cc_ids_add=noop_issue.cc_ids, cc_ids_remove=[333],
1982 blocked_on_add=noop_issue.blocked_on_iids,
1983 blocked_on_remove=[bo_remove_noop.issue_id])
1984 issue_delta_pairs = [(noop_issue, noop_delta)]
1985
1986 actual_tuple = tracker_helpers.ApplyAllIssueChanges(
1987 self.cnxn, issue_delta_pairs, self.services)
1988 expected_tuple = tracker_helpers._IssueChangesTuple(
1989 {}, {}, {}, {}, {}, {}, {}, {})
1990 self.assertEqual(actual_tuple, expected_tuple)
1991
1992 self.assertEqual(noop_issue, expected_noop_issue)
1993
1994 def testApplyAllIssueChanges_Empty(self):
1995 issue_delta_pairs = []
1996 actual_tuple = tracker_helpers.ApplyAllIssueChanges(
1997 self.cnxn, issue_delta_pairs, self.services)
1998 expected_tuple = tracker_helpers._IssueChangesTuple(
1999 {}, {}, {}, {}, {}, {}, {}, {})
2000 self.assertEqual(actual_tuple, expected_tuple)
2001
2002 def testUpdateClosedTimestamp(self):
2003 config = tracker_pb2.ProjectIssueConfig()
2004 config.well_known_statuses.append(
2005 tracker_pb2.StatusDef(status='New', means_open=True))
2006 config.well_known_statuses.append(
2007 tracker_pb2.StatusDef(status='Accepted', means_open=True))
2008 config.well_known_statuses.append(
2009 tracker_pb2.StatusDef(status='Old', means_open=False))
2010 config.well_known_statuses.append(
2011 tracker_pb2.StatusDef(status='Closed', means_open=False))
2012
2013 issue = tracker_pb2.Issue()
2014 issue.local_id = 1234
2015 issue.status = 'New'
2016
2017 # ensure the default value is undef
2018 self.assertTrue(not issue.closed_timestamp)
2019
2020 # ensure transitioning to the same and other open states
2021 # doesn't set the timestamp
2022 issue.status = 'New'
2023 tracker_helpers.UpdateClosedTimestamp(config, issue, 'New')
2024 self.assertTrue(not issue.closed_timestamp)
2025
2026 issue.status = 'Accepted'
2027 tracker_helpers.UpdateClosedTimestamp(config, issue, 'New')
2028 self.assertTrue(not issue.closed_timestamp)
2029
2030 # ensure transitioning from open to closed sets the timestamp
2031 issue.status = 'Closed'
2032 tracker_helpers.UpdateClosedTimestamp(config, issue, 'Accepted')
2033 self.assertTrue(issue.closed_timestamp)
2034
2035 # ensure that the timestamp is cleared when transitioning from
2036 # closed to open
2037 issue.status = 'New'
2038 tracker_helpers.UpdateClosedTimestamp(config, issue, 'Closed')
2039 self.assertTrue(not issue.closed_timestamp)
2040
2041 def testGroupUniqueDeltaIssues(self):
2042 """We can identify unique IssueDeltas and group Issues by their deltas."""
2043 issue_1 = _Issue('proj', 1)
2044 delta_1 = tracker_pb2.IssueDelta(cc_ids_add=[111])
2045
2046 issue_2 = _Issue('proj', 2)
2047 delta_2 = tracker_pb2.IssueDelta(cc_ids_add=[111], cc_ids_remove=[222])
2048
2049 issue_3 = _Issue('proj', 3)
2050 delta_3 = tracker_pb2.IssueDelta(cc_ids_add=[111])
2051
2052 issue_4 = _Issue('proj', 4)
2053 delta_4 = tracker_pb2.IssueDelta()
2054
2055 issue_5 = _Issue('proj', 5)
2056 delta_5 = tracker_pb2.IssueDelta()
2057
2058 issue_delta_pairs = [
2059 (issue_1, delta_1), (issue_2, delta_2), (issue_3, delta_3),
2060 (issue_4, delta_4), (issue_5, delta_5)
2061 ]
2062 unique_deltas, issues_for_deltas = tracker_helpers.GroupUniqueDeltaIssues(
2063 issue_delta_pairs)
2064
2065 expected_unique_deltas = [delta_1, delta_2, delta_4]
2066 self.assertEqual(unique_deltas, expected_unique_deltas)
2067 expected_issues_for_deltas = [
2068 [issue_1, issue_3], [issue_2], [issue_4, issue_5]
2069 ]
2070 self.assertEqual(issues_for_deltas, expected_issues_for_deltas)
2071
2072 def testEnforceAttachmentQuotaLimits(self):
2073 self.services.project.TestAddProject('Circe', project_id=798)
2074 issue_a1 = _Issue('Circe', 1, project_id=798)
2075 delta_a1 = tracker_pb2.IssueDelta()
2076
2077 issue_a2 = _Issue('Circe', 2, project_id=798)
2078 delta_a2 = tracker_pb2.IssueDelta()
2079
2080 self.services.project.TestAddProject('Patroclus', project_id=788)
2081 issue_b1 = _Issue('Patroclus', 1, project_id=788)
2082 delta_b1 = tracker_pb2.IssueDelta()
2083
2084 issue_delta_pairs = [
2085 (issue_a1, delta_a1), (issue_a2, delta_a2), (issue_b1, delta_b1)
2086 ]
2087
2088 upload_1 = framework_helpers.AttachmentUpload(
2089 'dragon', 'OOOOOO\n', 'text/plain')
2090 upload_2 = framework_helpers.AttachmentUpload(
2091 'snake', 'ooooo\n', 'text/plain')
2092 attachment_uploads = [upload_1, upload_2]
2093
2094 actual = tracker_helpers._EnforceAttachmentQuotaLimits(
2095 self.cnxn, issue_delta_pairs, self.services, attachment_uploads)
2096
2097 expected = {
2098 798: len(upload_1.contents + upload_2.contents) * 2,
2099 788: len(upload_1.contents + upload_2.contents)
2100 }
2101 self.assertEqual(actual, expected)
2102
2103 @mock.patch('tracker.tracker_constants.ISSUE_ATTACHMENTS_QUOTA_HARD', 1)
2104 def testEnforceAttachmentQuotaLimits_Exceeded(self):
2105 self.services.project.TestAddProject('Circe', project_id=798)
2106 issue_a1 = _Issue('Circe', 1, project_id=798)
2107 delta_a1 = tracker_pb2.IssueDelta()
2108
2109 issue_a2 = _Issue('Circe', 2, project_id=798)
2110 delta_a2 = tracker_pb2.IssueDelta()
2111
2112 self.services.project.TestAddProject('Patroclus', project_id=788)
2113 issue_b1 = _Issue('Patroclus', 1, project_id=788)
2114 delta_b1 = tracker_pb2.IssueDelta()
2115
2116 issue_delta_pairs = [
2117 (issue_a1, delta_a1), (issue_a2, delta_a2), (issue_b1, delta_b1)
2118 ]
2119
2120 upload_1 = framework_helpers.AttachmentUpload(
2121 'dragon', 'OOOOOO\n', 'text/plain')
2122 upload_2 = framework_helpers.AttachmentUpload(
2123 'snake', 'ooooo\n', 'text/plain')
2124 attachment_uploads = [upload_1, upload_2]
2125
2126 with self.assertRaisesRegexp(exceptions.OverAttachmentQuota,
2127 r'.+ project Patroclus\n.+ project Circe'):
2128 tracker_helpers._EnforceAttachmentQuotaLimits(
2129 self.cnxn, issue_delta_pairs, self.services, attachment_uploads)
2130
2131 def testAssertIssueChangesValid_Valid(self):
2132 """We can assert when deltas are valid for issues."""
2133 impacted_issue = _Issue('chicken', 101)
2134 self.services.issue.TestAddIssue(impacted_issue)
2135
2136 issue_1 = _Issue('chicken', 1)
2137 self.services.issue.TestAddIssue(issue_1)
2138 delta_1 = tracker_pb2.IssueDelta(
2139 merged_into=impacted_issue.issue_id, status='Duplicate')
2140 exp_d1 = copy.deepcopy(delta_1)
2141
2142 issue_2 = _Issue('chicken', 2)
2143 self.services.issue.TestAddIssue(issue_2)
2144 delta_2 = tracker_pb2.IssueDelta(blocked_on_add=[impacted_issue.issue_id])
2145 exp_d2 = copy.deepcopy(delta_2)
2146
2147 issue_3 = _Issue('chicken', 3)
2148 self.services.issue.TestAddIssue(issue_3)
2149 delta_3 = tracker_pb2.IssueDelta()
2150 exp_d3 = copy.deepcopy(delta_3)
2151
2152 issue_4 = _Issue('chicken', 4)
2153 self.services.issue.TestAddIssue(issue_4)
2154 delta_4 = tracker_pb2.IssueDelta(owner_id=self.project_member.user_id)
2155 exp_d4 = copy.deepcopy(delta_4)
2156
2157 issue_5 = _Issue('chicken', 5)
2158 self.services.issue.TestAddIssue(issue_5)
2159 fv = tracker_bizobj.MakeFieldValue(
2160 self.int_fd.field_id, 998, None, None, None, None, False)
2161 delta_5 = tracker_pb2.IssueDelta(field_vals_add=[fv])
2162 exp_d5 = copy.deepcopy(delta_5)
2163
2164 issue_6 = _Issue('chicken', 6)
2165 self.services.issue.TestAddIssue(issue_6)
2166 delta_6 = tracker_pb2.IssueDelta(
2167 summary=' ' + 's' * tracker_constants.MAX_SUMMARY_CHARS + ' ')
2168 exp_d6 = copy.deepcopy(delta_6)
2169
2170 issue_7 = _Issue('chicken', 7)
2171 self.services.issue.TestAddIssue(issue_7)
2172 issue_8 = _Issue('chicken', 8)
2173 self.services.issue.TestAddIssue(issue_8)
2174
2175 # We are fine with duplicate/consistent deltas.
2176 delta_7 = tracker_pb2.IssueDelta(blocked_on_add=[issue_8.issue_id])
2177 exp_d7 = copy.deepcopy(delta_7)
2178 delta_8 = tracker_pb2.IssueDelta(blocking_add=[issue_7.issue_id])
2179 exp_d8 = copy.deepcopy(delta_8)
2180
2181 issue_9 = _Issue('chicken', 9)
2182 self.services.issue.TestAddIssue(issue_9)
2183 issue_10 = _Issue('chicken', 10)
2184 self.services.issue.TestAddIssue(issue_10)
2185
2186 delta_9 = tracker_pb2.IssueDelta(blocked_on_remove=[issue_10.issue_id])
2187 exp_d9 = copy.deepcopy(delta_9)
2188 delta_10 = tracker_pb2.IssueDelta(blocking_remove=[issue_9.issue_id])
2189 exp_d10 = copy.deepcopy(delta_10)
2190
2191 issue_11 = _Issue('chicken', 11)
2192 user_fd = tracker_bizobj.MakeFieldDef(
2193 123, 789, 'CPU', tracker_pb2.FieldTypes.USER_TYPE, None, '', False,
2194 False, False, None, None, '', False, '', '',
2195 tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
2196 self.services.config.TestAddFieldDef(user_fd)
2197 a_user = self.services.user.TestAddUser('a_user@test.com', 123)
2198 delta_11 = tracker_pb2.IssueDelta(
2199 cc_ids_add=[222],
2200 field_vals_add=[
2201 tracker_bizobj.MakeFieldValue(
2202 user_fd.field_id, None, None, a_user.user_id, None, None, False)
2203 ])
2204 exp_d11 = copy.deepcopy(delta_11)
2205
2206 issue_delta_pairs = [
2207 (issue_1, delta_1), (issue_2, delta_2), (issue_3, delta_3),
2208 (issue_4, delta_4), (issue_5, delta_5), (issue_6, delta_6),
2209 (issue_7, delta_7), (issue_8, delta_8), (issue_9, delta_9),
2210 (issue_10, delta_10), (issue_11, delta_11)
2211 ]
2212 comment = ' ' + 'c' * tracker_constants.MAX_COMMENT_CHARS + ' '
2213 tracker_helpers._AssertIssueChangesValid(
2214 self.cnxn, issue_delta_pairs, self.services, comment_content=comment)
2215
2216 # Check we can handle None `comment_content`.
2217 tracker_helpers._AssertIssueChangesValid(
2218 self.cnxn, issue_delta_pairs, self.services)
2219 self.assertEqual(
2220 [
2221 exp_d1, exp_d2, exp_d3, exp_d4, exp_d5, exp_d6, exp_d7, exp_d8,
2222 exp_d9, exp_d10, exp_d11
2223 ], [
2224 delta_1, delta_2, delta_3, delta_4, delta_5, delta_6, delta_7,
2225 delta_8, delta_9, delta_10, delta_11
2226 ])
2227
2228 def testAssertIssueChangesValid_RequiredField(self):
2229 """Asserts fields and requried fields.."""
2230 issue_1 = _Issue('chicken', 1)
2231 self.services.issue.TestAddIssue(issue_1)
2232 delta_1 = tracker_pb2.IssueDelta()
2233 exp_d1 = copy.deepcopy(delta_1)
2234
2235 required_fd = tracker_bizobj.MakeFieldDef(
2236 124, 789, 'StrField', tracker_pb2.FieldTypes.STR_TYPE, None, '', True,
2237 False, False, None, None, '', False, '', '',
2238 tracker_pb2.NotifyTriggers.NEVER, 'no_action', 'doc', False)
2239 self.services.config.TestAddFieldDef(required_fd)
2240
2241 issue_delta_pairs = [(issue_1, delta_1)]
2242 comment = 'just a plain comment'
2243 tracker_helpers._AssertIssueChangesValid(
2244 self.cnxn, issue_delta_pairs, self.services, comment_content=comment)
2245
2246 # Check we can handle adding a field value when issue is in invalid state.
2247 fv = tracker_bizobj.MakeFieldValue(
2248 self.int_fd.field_id, 998, None, None, None, None, False)
2249 delta_2 = tracker_pb2.IssueDelta(field_vals_add=[fv])
2250 exp_d2 = copy.deepcopy(delta_2)
2251 tracker_helpers._AssertIssueChangesValid(
2252 self.cnxn, issue_delta_pairs, self.services)
2253 self.assertEqual([exp_d1, exp_d2], [delta_1, delta_2])
2254
2255 def testAssertIssueChangesValid_Invalid(self):
2256 """We can raise exceptions when deltas are not valid for issues. """
2257
2258 def getRef(issue):
2259 return '%s:%d' % (issue.project_name, issue.local_id)
2260
2261 issue_delta_pairs = []
2262 expected_err_msgs = []
2263
2264 comment = 'c' * (tracker_constants.MAX_COMMENT_CHARS + 1)
2265 expected_err_msgs.append('Comment is too long.')
2266
2267 issue_1 = _Issue('chicken', 1)
2268 self.services.issue.TestAddIssue(issue_1)
2269 issue_1_ref = getRef(issue_1)
2270
2271 delta_1 = tracker_pb2.IssueDelta(
2272 merged_into=issue_1.issue_id,
2273 blocked_on_add=[issue_1.issue_id],
2274 summary='',
2275 status='',
2276 cc_ids_add=[9876])
2277
2278 issue_delta_pairs.append((issue_1, delta_1))
2279 expected_err_msgs.extend(
2280 [
2281 ('%s: MERGED type statuses must accompany mergedInto values.') %
2282 issue_1_ref,
2283 '%s: Cannot merge an issue into itself.' % issue_1_ref,
2284 '%s: Cannot block an issue on itself.' % issue_1_ref,
2285 'users/9876: User does not exist.',
2286 '%s: Summary required.' % issue_1_ref,
2287 '%s: Status is required.' % issue_1_ref
2288 ])
2289
2290 issue_2 = _Issue('chicken', 2)
2291 self.services.issue.TestAddIssue(issue_2)
2292 issue_2_ref = getRef(issue_2)
2293
2294 fv = tracker_bizobj.MakeFieldValue(
2295 self.int_fd.field_id, 1000, None, None, None, None, False)
2296 delta_2 = tracker_pb2.IssueDelta(
2297 status='Duplicate',
2298 blocking_add=[issue_2.issue_id],
2299 summary='s' * (tracker_constants.MAX_SUMMARY_CHARS + 1),
2300 owner_id=self.no_project_user.user_id,
2301 field_vals_add=[fv])
2302 issue_delta_pairs.append((issue_2, delta_2))
2303
2304 expected_err_msgs.extend(
2305 [
2306 ('%s: MERGED type statuses must accompany mergedInto values.') %
2307 issue_2_ref,
2308 '%s: Cannot block an issue on itself.' % issue_2_ref,
2309 '%s: Issue owner must be a project member.' % issue_2_ref,
2310 '%s: Summary is too long.' % issue_2_ref,
2311 '%s: Error for %r: Value must be <= 999.' % (issue_2_ref, fv)
2312 ])
2313
2314 issue_3 = _Issue('chicken', 3)
2315 issue_3.status = 'Duplicate'
2316 issue_3.merged_into = 78911
2317 self.services.issue.TestAddIssue(issue_3)
2318 issue_3_ref = getRef(issue_3)
2319 delta_3 = tracker_pb2.IssueDelta(
2320 status='Available', merged_into_external='b/123')
2321 issue_delta_pairs.append((issue_3, delta_3))
2322 expected_err_msgs.append(
2323 '%s: MERGED type statuses must accompany mergedInto values.' %
2324 issue_3_ref)
2325
2326 with self.assertRaisesRegexp(exceptions.InputException,
2327 '\n'.join(expected_err_msgs)):
2328 tracker_helpers._AssertIssueChangesValid(
2329 self.cnxn, issue_delta_pairs, self.services, comment_content=comment)
2330
2331 def testAssertIssueChangesValid_ConflictingDeltas(self):
2332
2333 def getRef(issue):
2334 return '%s:%d' % (issue.project_name, issue.local_id)
2335
2336 expected_err_msgs = []
2337 issue_3 = _Issue('chicken', 3)
2338 self.services.issue.TestAddIssue(issue_3)
2339 issue_3_ref = getRef(issue_3)
2340 issue_4 = _Issue('chicken', 4)
2341 self.services.issue.TestAddIssue(issue_4)
2342 issue_4_ref = getRef(issue_4)
2343 issue_5 = _Issue('chicken', 5)
2344 self.services.issue.TestAddIssue(issue_5)
2345 issue_5_ref = getRef(issue_5)
2346 issue_6 = _Issue('chicken', 6)
2347 self.services.issue.TestAddIssue(issue_6)
2348 issue_6_ref = getRef(issue_6)
2349 issue_7 = _Issue('chicken', 7)
2350 self.services.issue.TestAddIssue(issue_7)
2351 issue_7_ref = getRef(issue_7)
2352
2353 delta_3 = tracker_pb2.IssueDelta(
2354 blocking_add=[issue_4.issue_id],
2355 blocked_on_add=[issue_5.issue_id, issue_6.issue_id])
2356
2357 delta_4 = tracker_pb2.IssueDelta(
2358 blocked_on_remove=[issue_3.issue_id], blocking_add=[issue_5.issue_id])
2359 expected_err_msgs.append(
2360 'Changes for %s conflict for %s' % (issue_4_ref, issue_3_ref))
2361
2362 delta_5 = tracker_pb2.IssueDelta(
2363 blocking_remove=[issue_3.issue_id],
2364 blocked_on_remove=[issue_4.issue_id])
2365 expected_err_msgs.append(
2366 'Changes for %s conflict for %s, %s' %
2367 (issue_5_ref, issue_3_ref, issue_4_ref))
2368
2369 delta_6 = tracker_pb2.IssueDelta(blocking_remove=[issue_3.issue_id])
2370 expected_err_msgs.append(
2371 'Changes for %s conflict for %s' % (issue_6_ref, issue_3_ref))
2372
2373 impacted_issue = _Issue('chicken', 11)
2374 self.services.issue.TestAddIssue(impacted_issue)
2375 impacted_issue_ref = getRef(impacted_issue)
2376 delta_7 = tracker_pb2.IssueDelta(
2377 blocking_remove=[issue_3.issue_id],
2378 blocking_add=[issue_3.issue_id],
2379 blocked_on_remove=[impacted_issue.issue_id],
2380 blocked_on_add=[impacted_issue.issue_id])
2381 expected_err_msgs.append(
2382 'Changes for %s conflict for %s, %s' %
2383 (issue_7_ref, issue_3_ref, impacted_issue_ref))
2384
2385 issue_delta_pairs = [
2386 (issue_3, delta_3),
2387 (issue_4, delta_4),
2388 (issue_5, delta_5),
2389 (issue_6, delta_6),
2390 (issue_7, delta_7),
2391 ]
2392
2393 with self.assertRaisesRegexp(exceptions.InputException,
2394 '\n'.join(expected_err_msgs)):
2395 tracker_helpers._AssertIssueChangesValid(
2396 self.cnxn, issue_delta_pairs, self.services)
2397
2398 def testComputeNewCcsFromIssueMerge(self):
2399 """We can compute the new ccs to add to a merge-into issue."""
2400 target_issue = fake.MakeTestIssue(789, 10, 'Target issue', 'New', 111)
2401 source_issue_1 = fake.MakeTestIssue(
2402 789, 11, 'Source issue', 'New', 111) # different restrictions
2403 source_issue_2 = fake.MakeTestIssue(
2404 789, 12, 'Source issue', 'New', 222) # same restrictions
2405 source_issue_3 = fake.MakeTestIssue(
2406 789, 13, 'Source issue', 'New', 222) # no restrictions
2407 source_issue_4 = fake.MakeTestIssue(
2408 789, 14, 'Source issue', 'New', 666) # empty ccs
2409 source_issue_5 = fake.MakeTestIssue(
2410 788, 15, 'Source issue', 'New', 666) # different project
2411 source_issue_1.cc_ids.append(333)
2412 source_issue_2.cc_ids.append(444)
2413 source_issue_3.cc_ids.append(555)
2414 source_issue_5.cc_ids.append(999)
2415
2416 target_issue.labels.append('Restrict-View-Chicken')
2417 source_issue_1.labels.append('Restrict-View-Cow')
2418 source_issue_2.labels.append('Restrict-View-Chicken')
2419
2420 self.services.issue.TestAddIssue(target_issue)
2421 self.services.issue.TestAddIssue(source_issue_1)
2422 self.services.issue.TestAddIssue(source_issue_2)
2423 self.services.issue.TestAddIssue(source_issue_3)
2424 self.services.issue.TestAddIssue(source_issue_4)
2425 self.services.issue.TestAddIssue(source_issue_5)
2426
2427 new_cc_ids = tracker_helpers._ComputeNewCcsFromIssueMerge(
2428 target_issue, [source_issue_1, source_issue_2, source_issue_3])
2429 self.assertItemsEqual(new_cc_ids, [444, 555, 222])
2430
2431 def testComputeNewCcsFromIssueMerge_Empty(self):
2432 target_issue = fake.MakeTestIssue(789, 10, 'Target issue', 'New', 111)
2433 self.services.issue.TestAddIssue(target_issue)
2434 new_cc_ids = tracker_helpers._ComputeNewCcsFromIssueMerge(target_issue, [])
2435 self.assertItemsEqual(new_cc_ids, [])
2436
2437 def testEnforceNonMergeStatusDeltas(self):
2438 # No updates: user is setting to a non-MERGED status with no
2439 # existing merged_into values.
2440 issue_1 = _Issue('chicken', 1)
2441 self.services.issue.TestAddIssue(issue_1)
2442 delta_1 = tracker_pb2.IssueDelta(status='Available')
2443 exp_delta_1 = copy.deepcopy(delta_1)
2444
2445 # No updates: user is setting to a MERGED status. Whether this request
2446 # goes through will be handled by _AssertIssueChangesValid().
2447 issue_2 = _Issue('chicken', 2)
2448 self.services.issue.TestAddIssue(issue_2)
2449 delta_2 = tracker_pb2.IssueDelta(status='Duplicate')
2450 exp_delta_2 = copy.deepcopy(delta_2)
2451
2452 # No updates: user is setting to a MERGED status. (This test issue starts
2453 # out with a merged_into value but a non-MERGED status. We don't expect
2454 # real data to ever be in this state)
2455 issue_3 = _Issue('chicken', 3)
2456 issue_3.merged_into = 7011
2457 self.services.issue.TestAddIssue(issue_3)
2458 delta_3 = tracker_pb2.IssueDelta(status='Duplicate')
2459 exp_delta_3 = copy.deepcopy(delta_3)
2460
2461 # No updates: same situation as above.
2462 issue_4 = _Issue('chicken', 4)
2463 issue_4.merged_into_external = 'b/123'
2464 self.services.issue.TestAddIssue(issue_4)
2465 delta_4 = tracker_pb2.IssueDelta(status='Duplicate')
2466 exp_delta_4 = copy.deepcopy(delta_4)
2467
2468 # Update delta: user is setting status AWAY from a MERGED status, so we
2469 # auto-remove any existing merged_into values.
2470 issue_5 = _Issue('chicken', 5)
2471 issue_5.merged_into = 7011
2472 self.services.issue.TestAddIssue(issue_5)
2473 delta_5 = tracker_pb2.IssueDelta(status='Available')
2474 exp_delta_5 = copy.deepcopy(delta_5)
2475 exp_delta_5.merged_into = 0
2476
2477 # Update delta: user is setting status AWAY from a MERGED status, so we
2478 # auto-remove any existing merged_into values.
2479 issue_6 = _Issue('chicken', 6)
2480 issue_6.merged_into_external = 'b/123'
2481 self.services.issue.TestAddIssue(issue_6)
2482 delta_6 = tracker_pb2.IssueDelta(status='Available')
2483 exp_delta_6 = copy.deepcopy(delta_6)
2484 exp_delta_6.merged_into_external = ''
2485
2486 # No updates: user is setting to a non-MERGED status while also setting
2487 # a merged_into value. This will be rejected down the line by
2488 # _AssertIssueChangesValid()
2489 issue_7 = _Issue('chicken', 7)
2490 issue_7.merged_into = 7011
2491 self.services.issue.TestAddIssue(issue_7)
2492 delta_7 = tracker_pb2.IssueDelta(
2493 merged_into_external='b/123', status='Available')
2494 exp_delta_7 = copy.deepcopy(delta_7)
2495
2496 # No updates: user is setting to a non-MERGED status while also setting
2497 # a merged_into value. This will be rejected down the line by
2498 # _AssertIssueChangesValid()
2499 issue_8 = _Issue('chicken', 8)
2500 issue_8.merged_into_external = 'b/123'
2501 self.services.issue.TestAddIssue(issue_8)
2502 delta_8 = tracker_pb2.IssueDelta(merged_into=8011, status='Available')
2503 exp_delta_8 = copy.deepcopy(delta_8)
2504
2505 pairs = [
2506 (issue_1, delta_1), (issue_2, delta_2), (issue_3, delta_3),
2507 (issue_4, delta_4), (issue_5, delta_5), (issue_6, delta_6),
2508 (issue_7, delta_7), (issue_8, delta_8)
2509 ]
2510
2511 tracker_helpers._EnforceNonMergeStatusDeltas(
2512 self.cnxn, pairs, self.services)
2513 self.assertEqual(
2514 [
2515 delta_1, delta_2, delta_3, delta_4, delta_5, delta_6, delta_7,
2516 delta_8
2517 ], [
2518 exp_delta_1, exp_delta_2, exp_delta_3, exp_delta_4, exp_delta_5,
2519 exp_delta_6, exp_delta_7, exp_delta_8
2520 ])
2521
2522
2523class IssueChangeImpactedIssuesTest(unittest.TestCase):
2524 """Tests for the _IssueChangeImpactedIssues class."""
2525
2526 def setUp(self):
2527 self.services = service_manager.Services(
2528 issue=fake.IssueService(), issue_star=fake.IssueStarService())
2529 self.cnxn = 'fake connection'
2530
2531 def testComputeAllImpactedIDs(self):
2532 tracker = tracker_helpers._IssueChangeImpactedIssues()
2533 tracker.blocking_add[78901].append(1)
2534 tracker.blocking_remove[78902].append(2)
2535 tracker.blocked_on_add[78903].append(1)
2536 tracker.blocked_on_remove[78904].append(1)
2537 tracker.merged_from_add[78905].append(3)
2538 tracker.merged_from_remove[78906].append(3)
2539
2540 # Repeat a few iids.
2541 tracker.blocked_on_remove[78901].append(1)
2542 tracker.merged_from_add[78903].append(1)
2543
2544 actual = tracker.ComputeAllImpactedIIDs()
2545 expected = {78901, 78902, 78903, 78904, 78905, 78906}
2546 self.assertEqual(actual, expected)
2547
2548 def testComputeAllImpactedIDs_Empty(self):
2549 tracker = tracker_helpers._IssueChangeImpactedIssues()
2550 actual = tracker.ComputeAllImpactedIIDs()
2551 self.assertEqual(actual, set())
2552
2553 def testTrackImpactedIssues(self):
2554 issue_delta_pairs = []
2555
2556 issue_1 = _Issue('project', 1)
2557 issue_1.merged_into = 78906
2558 delta_1 = tracker_pb2.IssueDelta(
2559 merged_into=78905,
2560 blocked_on_add=[78901, 78902],
2561 blocked_on_remove=[78903, 78904],
2562 )
2563 issue_delta_pairs.append((issue_1, delta_1))
2564
2565 issue_2 = _Issue('project', 2)
2566 issue_2.merged_into = 78905
2567 delta_2 = tracker_pb2.IssueDelta(
2568 merged_into=78905, # This should be ignored.
2569 blocking_add=[78901, 78902],
2570 blocking_remove=[78903, 78904],
2571 )
2572 issue_delta_pairs.append((issue_2, delta_2))
2573
2574 issue_3 = _Issue('project', 3)
2575 issue_3.merged_into = 78902
2576 delta_3 = tracker_pb2.IssueDelta(merged_into=78901)
2577 issue_delta_pairs.append((issue_3, delta_3))
2578
2579 issue_4 = _Issue('project', 4)
2580 issue_4.merged_into = 78901
2581 delta_4 = tracker_pb2.IssueDelta(
2582 merged_into=framework_constants.NO_ISSUE_SPECIFIED)
2583 issue_delta_pairs.append((issue_4, delta_4))
2584
2585 impacted_issues = tracker_helpers._IssueChangeImpactedIssues()
2586 for issue, delta in issue_delta_pairs:
2587 impacted_issues.TrackImpactedIssues(issue, delta)
2588
2589 self.assertEqual(
2590 impacted_issues.blocking_add, {
2591 78901: [issue_1.issue_id],
2592 78902: [issue_1.issue_id]
2593 })
2594 self.assertEqual(
2595 impacted_issues.blocking_remove, {
2596 78903: [issue_1.issue_id],
2597 78904: [issue_1.issue_id]
2598 })
2599 self.assertEqual(
2600 impacted_issues.blocked_on_add, {
2601 78901: [issue_2.issue_id],
2602 78902: [issue_2.issue_id]
2603 })
2604 self.assertEqual(
2605 impacted_issues.blocked_on_remove, {
2606 78903: [issue_2.issue_id],
2607 78904: [issue_2.issue_id]
2608 })
2609 self.assertEqual(
2610 impacted_issues.merged_from_add, {
2611 78901: [issue_3.issue_id],
2612 78905: [issue_1.issue_id],
2613 })
2614 self.assertEqual(
2615 impacted_issues.merged_from_remove, {
2616 78901: [issue_4.issue_id],
2617 78902: [issue_3.issue_id],
2618 78906: [issue_1.issue_id],
2619 })
2620
2621 def testApplyImpactedIssueChanges(self):
2622 impacted_tracker = tracker_helpers._IssueChangeImpactedIssues()
2623 impacted_issue = _Issue('proj', 1)
2624 self.services.issue.TestAddIssue(impacted_issue)
2625 impacted_iid = impacted_issue.issue_id
2626
2627 # Setup.
2628 bo_add = _Issue('proj', 2)
2629 self.services.issue.TestAddIssue(bo_add)
2630 impacted_tracker.blocked_on_add[impacted_iid].append(bo_add.issue_id)
2631
2632 bo_remove = _Issue('proj', 3)
2633 self.services.issue.TestAddIssue(bo_remove)
2634 impacted_tracker.blocked_on_remove[impacted_iid].append(
2635 bo_remove.issue_id)
2636
2637 b_add = _Issue('proj', 4)
2638 self.services.issue.TestAddIssue(b_add)
2639 impacted_tracker.blocking_add[impacted_iid].append(
2640 b_add.issue_id)
2641
2642 b_remove = _Issue('proj', 5)
2643 self.services.issue.TestAddIssue(b_remove)
2644 impacted_tracker.blocking_remove[impacted_iid].append(
2645 b_remove.issue_id)
2646
2647 m_add = _Issue('proj', 6)
2648 m_add.cc_ids = [666, 777]
2649 self.services.issue.TestAddIssue(m_add)
2650 m_add_no_ccs = _Issue('proj', 7, '', '')
2651 self.services.issue.TestAddIssue(m_add_no_ccs)
2652 impacted_tracker.merged_from_add[impacted_iid].extend(
2653 [m_add.issue_id, m_add_no_ccs.issue_id])
2654 # Set up starrers.
2655 self.services.issue_star.SetStar(
2656 self.cnxn, self.services, None, impacted_iid, 111, True)
2657 self.services.issue_star.SetStar(
2658 self.cnxn, self.services, None, impacted_iid, 222, True)
2659 self.services.issue_star.SetStar(
2660 self.cnxn, self.services, None, m_add.issue_id, 222, True)
2661 self.services.issue_star.SetStar(
2662 self.cnxn, self.services, None, m_add.issue_id, 333, True)
2663 self.services.issue_star.SetStar(
2664 self.cnxn, self.services, None, m_add.issue_id, 444, True)
2665
2666 m_remove = _Issue('proj', 8)
2667 m_remove.cc_ids = [888]
2668 self.services.issue.TestAddIssue(m_remove)
2669 impacted_tracker.merged_from_remove[impacted_iid].append(
2670 m_remove.issue_id)
2671
2672
2673 impacted_issue.cc_ids = [666]
2674 impacted_issue.blocked_on_iids = [78404, bo_remove.issue_id]
2675 impacted_issue.blocking_iids = [78405, b_remove.issue_id]
2676 expected_issue = copy.deepcopy(impacted_issue)
2677
2678 # Verify.
2679 (actual_amendments,
2680 actual_new_starrers) = impacted_tracker.ApplyImpactedIssueChanges(
2681 self.cnxn, impacted_issue, self.services)
2682 expected_amendments = [
2683 tracker_bizobj.MakeBlockedOnAmendment(
2684 [('proj', bo_add.local_id)],
2685 [('proj', bo_remove.local_id)], default_project_name='proj'),
2686 tracker_bizobj.MakeBlockingAmendment(
2687 [('proj', b_add.local_id)],
2688 [('proj', b_remove.local_id)], default_project_name='proj'),
2689 tracker_bizobj.MakeCcAmendment([777], []),
2690 tracker_bizobj.MakeMergedIntoAmendment(
2691 [('proj', m_add.local_id), ('proj', m_add_no_ccs.local_id)],
2692 [('proj', m_remove.local_id)], default_project_name='proj')
2693 ]
2694 self.assertEqual(actual_amendments, expected_amendments)
2695 self.assertItemsEqual(actual_new_starrers, [333, 444])
2696
2697 expected_issue.cc_ids.append(777)
2698 expected_issue.blocked_on_iids = [78404, bo_add.issue_id]
2699 # By default new blocked_on issues that appear in blocked_on_iids
2700 # with no prior rank associated with it are un-ranked and assigned rank 0.
2701 # See SortBlockedOn in issue_svc.py.
2702 expected_issue.blocked_on_ranks = [0, 0]
2703 expected_issue.blocking_iids = [78405, b_add.issue_id]
2704 expected_issue.star_count = 4
2705 self.assertEqual(impacted_issue, expected_issue)
2706
2707 def testApplyImpactedIssueChanges_Empty(self):
2708 impacted_tracker = tracker_helpers._IssueChangeImpactedIssues()
2709 impacted_issue = _Issue('proj', 1)
2710 expected_issue = copy.deepcopy(impacted_issue)
2711
2712 (actual_amendments,
2713 actual_new_starrers) = impacted_tracker.ApplyImpactedIssueChanges(
2714 self.cnxn, impacted_issue, self.services)
2715
2716 expected_amendments = []
2717 self.assertEqual(actual_amendments, expected_amendments)
2718 expected_new_starrers = []
2719 self.assertEqual(actual_new_starrers, expected_new_starrers)
2720 self.assertEqual(impacted_issue, expected_issue)
2721
2722 def testApplyImpactedIssueChanges_PartiallyEmptyMergedFrom(self):
2723 """We can process merged_from changes when one of the lists is empty."""
2724 impacted_tracker = tracker_helpers._IssueChangeImpactedIssues()
2725 impacted_issue = _Issue('proj', 1)
2726 impacted_iid = impacted_issue.issue_id
2727 expected_issue = copy.deepcopy(impacted_issue)
2728
2729 m_add = _Issue('proj', 2)
2730 self.services.issue.TestAddIssue(m_add)
2731 impacted_tracker.merged_from_add[impacted_iid].append(
2732 m_add.issue_id)
2733 # We're leaving impacted_tracker.merged_from_remove empty.
2734
2735 (actual_amendments,
2736 actual_new_starrers) = impacted_tracker.ApplyImpactedIssueChanges(
2737 self.cnxn, impacted_issue, self.services)
2738
2739 expected_amendments = [tracker_bizobj.MakeMergedIntoAmendment(
2740 [('proj', m_add.local_id)], [], default_project_name='proj')]
2741 self.assertEqual(actual_amendments, expected_amendments)
2742 expected_new_starrers = []
2743 self.assertEqual(actual_new_starrers, expected_new_starrers)
2744 self.assertEqual(impacted_issue, expected_issue)
2745
2746
2747class AssertUsersExistTest(unittest.TestCase):
2748
2749 def setUp(self):
2750 self.cnxn = 'fake cnxn'
2751 self.services = service_manager.Services(user=fake.UserService())
2752 for user_id in [1, 1001, 1002, 1003, 2001, 2002, 3002]:
2753 self.services.user.TestAddUser('test%d' % user_id, user_id, add_user=True)
2754
2755 def test_AssertUsersExist_Passes(self):
2756 existing = [1, 1001, 1002, 1003, 2001, 2002, 3002]
2757 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
2758 tracker_helpers.AssertUsersExist(
2759 self.cnxn, self.services, existing, err_agg)
2760
2761 def test_AssertUsersExist_Empty(self):
2762 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
2763 tracker_helpers.AssertUsersExist(
2764 self.cnxn, self.services, [], err_agg)
2765
2766 def test_AssertUsersExist(self):
2767 dne_users = [2, 3]
2768 existing = [1, 1001, 1002, 1003, 2001, 2002, 3002]
2769 all_users = existing + dne_users
2770 with self.assertRaisesRegexp(
2771 exceptions.InputException,
2772 'users/2: User does not exist.\nusers/3: User does not exist.'):
2773 with exceptions.ErrorAggregator(exceptions.InputException) as err_agg:
2774 tracker_helpers.AssertUsersExist(
2775 self.cnxn, self.services, all_users, err_agg)