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