blob: cb0101457a498c28311fdd9ca1fad29c84470003 [file] [log] [blame]
Copybara854996b2021-09-07 19:36:02 +00001# Copyright 2020 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style
3# license that can be found in the LICENSE file or at
4# https://developers.google.com/open-source/licenses/bsd
5
6"""Tests for the issues servicer."""
7from __future__ import print_function
8from __future__ import division
9from __future__ import absolute_import
10
11import copy
12import unittest
13import mock
14
15from api.v3 import converters
16from api.v3 import issues_servicer
17from api.v3.api_proto import issues_pb2
18from api.v3.api_proto import issue_objects_pb2
19from framework import exceptions
20from framework import framework_helpers
21from framework import monorailcontext
22from framework import permissions
23from proto import tracker_pb2
24from testing import fake
25from services import service_manager
26
27from google.appengine.ext import testbed
28from google.protobuf import timestamp_pb2
29from google.protobuf import field_mask_pb2
30
31
32def _Issue(project_id, local_id):
33 issue = tracker_pb2.Issue(owner_id=0)
34 issue.project_name = 'proj-%d' % project_id
35 issue.project_id = project_id
36 issue.local_id = local_id
37 issue.issue_id = project_id * 100 + local_id
38 return issue
39
40
41CURRENT_TIME = 12346.78
42
43
44class IssuesServicerTest(unittest.TestCase):
45
46 def setUp(self):
47 # memcache and datastore needed for generating page tokens.
48 self.testbed = testbed.Testbed()
49 self.testbed.activate()
50 self.testbed.init_memcache_stub()
51 self.testbed.init_datastore_v3_stub()
52
53 self.cnxn = fake.MonorailConnection()
54 self.services = service_manager.Services(
55 config=fake.ConfigService(),
56 issue=fake.IssueService(),
57 issue_star=fake.IssueStarService(),
58 project=fake.ProjectService(),
59 features=fake.FeaturesService(),
60 spam=fake.SpamService(),
61 user=fake.UserService(),
62 usergroup=fake.UserGroupService())
63 self.issues_svcr = issues_servicer.IssuesServicer(
64 self.services, make_rate_limiter=False)
65 self.PAST_TIME = int(CURRENT_TIME - 1)
66
67 self.owner = self.services.user.TestAddUser('owner@example.com', 111)
68 self.user_2 = self.services.user.TestAddUser('user_2@example.com', 222)
69
70 self.project_1 = self.services.project.TestAddProject(
71 'chicken', project_id=789)
72 self.issue_1_resource_name = 'projects/chicken/issues/1234'
73 self.issue_1 = fake.MakeTestIssue(
74 self.project_1.project_id,
75 1234,
76 'sum',
77 'New',
78 self.owner.user_id,
79 labels=['find-me', 'pri-3'],
80 project_name=self.project_1.project_name)
81 self.services.issue.TestAddIssue(self.issue_1)
82
83 self.project_2 = self.services.project.TestAddProject('cow', project_id=788)
84 self.issue_2_resource_name = 'projects/cow/issues/1235'
85 self.issue_2 = fake.MakeTestIssue(
86 self.project_2.project_id,
87 1235,
88 'sum',
89 'New',
90 self.user_2.user_id,
91 project_name=self.project_2.project_name)
92 self.services.issue.TestAddIssue(self.issue_2)
93 self.issue_3 = fake.MakeTestIssue(
94 self.project_2.project_id,
95 1236,
96 'sum',
97 'New',
98 self.user_2.user_id,
99 labels=['find-me', 'pri-1'],
100 project_name=self.project_2.project_name)
101 self.services.issue.TestAddIssue(self.issue_3)
102
103 def CallWrapped(self, wrapped_handler, mc, *args, **kwargs):
104 self.issues_svcr.converter = converters.Converter(mc, self.services)
105 return wrapped_handler.wrapped(self.issues_svcr, mc, *args, **kwargs)
106
107 def testGetIssue(self):
108 """We can get an issue."""
109 request = issues_pb2.GetIssueRequest(name=self.issue_1_resource_name)
110 mc = monorailcontext.MonorailContext(
111 self.services, cnxn=self.cnxn, requester=self.owner.email)
112 actual_response = self.CallWrapped(self.issues_svcr.GetIssue, mc, request)
113 self.assertEqual(
114 actual_response, self.issues_svcr.converter.ConvertIssue(self.issue_1))
115
116 def testBatchGetIssues(self):
117 """We can batch get issues."""
118 mc = monorailcontext.MonorailContext(
119 self.services, cnxn=self.cnxn, requester=self.owner.email)
120 request = issues_pb2.BatchGetIssuesRequest(
121 names=['projects/cow/issues/1235', 'projects/cow/issues/1236'])
122 actual_response = self.CallWrapped(
123 self.issues_svcr.BatchGetIssues, mc, request)
124 self.assertEqual(
125 [issue.name for issue in actual_response.issues],
126 ['projects/cow/issues/1235', 'projects/cow/issues/1236'])
127
128 def testBatchGetIssues_Empty(self):
129 """We can return a response if the request has no names."""
130 mc = monorailcontext.MonorailContext(
131 self.services, cnxn=self.cnxn, requester=self.owner.email)
132 request = issues_pb2.BatchGetIssuesRequest(names=[])
133 actual_response = self.CallWrapped(
134 self.issues_svcr.BatchGetIssues, mc, request)
135 self.assertEqual(
136 actual_response, issues_pb2.BatchGetIssuesResponse(issues=[]))
137
138 def testBatchGetIssues_WithParent(self):
139 """We can batch get issues with a given parent."""
140 mc = monorailcontext.MonorailContext(
141 self.services, cnxn=self.cnxn, requester=self.owner.email)
142 request = issues_pb2.BatchGetIssuesRequest(
143 parent='projects/cow',
144 names=['projects/cow/issues/1235', 'projects/cow/issues/1236'])
145 actual_response = self.CallWrapped(
146 self.issues_svcr.BatchGetIssues, mc, request)
147 self.assertEqual(
148 [issue.name for issue in actual_response.issues],
149 ['projects/cow/issues/1235', 'projects/cow/issues/1236'])
150
151 def testBatchGetIssues_FromMultipleProjects(self):
152 """We can batch get issues from multiple projects."""
153 mc = monorailcontext.MonorailContext(
154 self.services, cnxn=self.cnxn, requester=self.owner.email)
155 request = issues_pb2.BatchGetIssuesRequest(
156 names=[
157 'projects/chicken/issues/1234', 'projects/cow/issues/1235',
158 'projects/cow/issues/1236'
159 ])
160 actual_response = self.CallWrapped(
161 self.issues_svcr.BatchGetIssues, mc, request)
162 self.assertEqual(
163 [issue.name for issue in actual_response.issues], [
164 'projects/chicken/issues/1234', 'projects/cow/issues/1235',
165 'projects/cow/issues/1236'
166 ])
167
168 def testBatchGetIssues_WithBadInput(self):
169 """We raise an exception with bad input to batch get issues."""
170 mc = monorailcontext.MonorailContext(
171 self.services, cnxn=self.cnxn, requester=self.owner.email)
172 request = issues_pb2.BatchGetIssuesRequest(
173 parent='projects/cow',
174 names=['projects/cow/issues/1235', 'projects/chicken/issues/1234'])
175 with self.assertRaisesRegexp(
176 exceptions.InputException,
177 'projects/chicken/issues/1234 is not a child issue of projects/cow.'):
178 self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
179
180 request = issues_pb2.BatchGetIssuesRequest(
181 parent='projects/sheep',
182 names=['projects/cow/issues/1235', 'projects/chicken/issues/1234'])
183 with self.assertRaisesRegexp(
184 exceptions.InputException,
185 'projects/cow/issues/1235 is not a child issue of projects/sheep.\n' +
186 'projects/chicken/issues/1234 is not a child issue of projects/sheep.'):
187 self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
188
189 request = issues_pb2.BatchGetIssuesRequest(
190 parent='projects/cow',
191 names=['projects/cow/badformat/1235', 'projects/chicken/issues/1234'])
192 with self.assertRaisesRegexp(
193 exceptions.InputException,
194 'Invalid resource name: projects/cow/badformat/1235.'):
195 self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
196
197 def testBatchGetIssues_NonExistentIssues(self):
198 """We raise an exception with bad input to batch get issues."""
199 mc = monorailcontext.MonorailContext(
200 self.services, cnxn=self.cnxn, requester=self.owner.email)
201 request = issues_pb2.BatchGetIssuesRequest(
202 parent='projects/chicken',
203 names=['projects/chicken/issues/1', 'projects/chicken/issues/2'])
204 with self.assertRaisesRegexp(
205 exceptions.NoSuchIssueException,
206 "\['projects/chicken/issues/1', 'projects/chicken/issues/2'\] not found"
207 ):
208 self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
209
210 @mock.patch('api.v3.api_constants.MAX_BATCH_ISSUES', 2)
211 def testBatchGetIssues(self):
212 mc = monorailcontext.MonorailContext(
213 self.services, cnxn=self.cnxn, requester=self.owner.email)
214 request = issues_pb2.BatchGetIssuesRequest(
215 parent='projects/cow',
216 names=[
217 'projects/cow/issues/1235', 'projects/chicken/issues/1234',
218 'projects/cow/issues/1233'
219 ])
220 with self.assertRaises(exceptions.InputException):
221 self.CallWrapped(self.issues_svcr.BatchGetIssues, mc, request)
222
223 @mock.patch('search.frontendsearchpipeline.FrontendSearchPipeline')
224 @mock.patch('api.v3.api_constants.MAX_ISSUES_PER_PAGE', 2)
225 def testSearchIssues(self, mock_pipeline):
226 """We can search for issues in some projects."""
227 request = issues_pb2.SearchIssuesRequest(
228 projects=['projects/chicken', 'projects/cow'],
229 query='label:find-me',
230 order_by='-pri',
231 page_size=3)
232 mc = monorailcontext.MonorailContext(
233 self.services, cnxn=self.cnxn, requester=self.user_2.email)
234
235 instance = mock.Mock(
236 spec=True, total_count=3, visible_results=[self.issue_1, self.issue_3])
237 mock_pipeline.return_value = instance
238 instance.SearchForIIDs = mock.Mock()
239 instance.MergeAndSortIssues = mock.Mock()
240 instance.Paginate = mock.Mock()
241
242 actual_response = self.CallWrapped(
243 self.issues_svcr.SearchIssues, mc, request)
244 # start index is 0.
245 # number of items is coerced from 3 -> 2
246 mock_pipeline.assert_called_once_with(
247 self.cnxn,
248 self.services,
249 mc.auth, [222],
250 'label:find-me', ['chicken', 'cow'],
251 2,
252 0,
253 1,
254 '',
255 '-pri',
256 mc.warnings,
257 mc.errors,
258 True,
259 mc.profiler,
260 project=None)
261 self.assertEqual(
262 [issue.name for issue in actual_response.issues],
263 ['projects/chicken/issues/1234', 'projects/cow/issues/1236'])
264
265 # Check the `next_page_token` can be used to get the next page of results.
266 request.page_token = actual_response.next_page_token
267 self.CallWrapped(self.issues_svcr.SearchIssues, mc, request)
268 # start index is now 2.
269 mock_pipeline.assert_called_with(
270 self.cnxn,
271 self.services,
272 mc.auth, [222],
273 'label:find-me', ['chicken', 'cow'],
274 2,
275 2,
276 1,
277 '',
278 '-pri',
279 mc.warnings,
280 mc.errors,
281 True,
282 mc.profiler,
283 project=None)
284
285 @mock.patch('search.frontendsearchpipeline.FrontendSearchPipeline')
286 @mock.patch('api.v3.api_constants.MAX_ISSUES_PER_PAGE', 2)
287 def testSearchIssues_PaginationErrorOrderByChanged(self, mock_pipeline):
288 """Error when changing the order_by and using the same page_otoken."""
289 request = issues_pb2.SearchIssuesRequest(
290 projects=['projects/chicken', 'projects/cow'],
291 query='label:find-me',
292 order_by='-pri',
293 page_size=3)
294 mc = monorailcontext.MonorailContext(
295 self.services, cnxn=self.cnxn, requester=self.user_2.email)
296
297 instance = mock.Mock(
298 spec=True, total_count=3, visible_results=[self.issue_1, self.issue_3])
299 mock_pipeline.return_value = instance
300 instance.SearchForIIDs = mock.Mock()
301 instance.MergeAndSortIssues = mock.Mock()
302 instance.Paginate = mock.Mock()
303
304 actual_response = self.CallWrapped(
305 self.issues_svcr.SearchIssues, mc, request)
306
307 # The request should fail if we use `next_page_token` and change parameters.
308 request.page_token = actual_response.next_page_token
309 request.order_by = 'owner'
310 with self.assertRaises(exceptions.PageTokenException):
311 self.CallWrapped(self.issues_svcr.SearchIssues, mc, request)
312
313 # Note the 'empty' case doesn't make sense for ListComments, as one is created
314 # for every issue.
315 def testListComments(self):
316 comment_2 = tracker_pb2.IssueComment(
317 id=123,
318 issue_id=self.issue_1.issue_id,
319 project_id=self.issue_1.project_id,
320 user_id=self.owner.user_id,
321 content='comment 2')
322 self.services.issue.TestAddComment(comment_2, self.issue_1.local_id)
323 request = issues_pb2.ListCommentsRequest(
324 parent=self.issue_1_resource_name, page_size=1)
325 mc = monorailcontext.MonorailContext(
326 self.services, cnxn=self.cnxn, requester=self.owner.email)
327 actual_response = self.CallWrapped(
328 self.issues_svcr.ListComments, mc, request)
329 self.assertEqual(len(actual_response.comments), 1)
330
331 # Check the `next_page_token` can be used to get the next page of results
332 request.page_token = actual_response.next_page_token
333 next_actual_response = self.CallWrapped(
334 self.issues_svcr.ListComments, mc, request)
335 self.assertEqual(len(next_actual_response.comments), 1)
336 self.assertEqual(next_actual_response.comments[0].content, 'comment 2')
337
338 def testListComments_UnsupportedFilter(self):
339 """If anything other than approval is provided, it's an error."""
340 filter_str = 'content = "x"'
341 request = issues_pb2.ListCommentsRequest(
342 parent=self.issue_1_resource_name, page_size=1, filter=filter_str)
343 mc = monorailcontext.MonorailContext(
344 self.services, cnxn=self.cnxn, requester=self.owner.email)
345 with self.assertRaises(exceptions.InputException):
346 self.CallWrapped(self.issues_svcr.ListComments, mc, request)
347
348 def testListComments_TwoApprovalsErrors(self):
349 """If anything other than a single approval is provided, it's an error."""
350 filter_str = (
351 'approval = "projects/chicken/approvalDefs/404" OR '
352 'approval = "projects/chicken/approvalDefs/405')
353 request = issues_pb2.ListCommentsRequest(
354 parent=self.issue_1_resource_name, page_size=1, filter=filter_str)
355 mc = monorailcontext.MonorailContext(
356 self.services, cnxn=self.cnxn, requester=self.owner.email)
357 with self.assertRaises(exceptions.InputException):
358 self.CallWrapped(self.issues_svcr.ListComments, mc, request)
359
360 def testListComments_FilterTypoError(self):
361 """Even an extra space is an error."""
362 filter_str = 'approval = "projects/chicken/approvalDefs/404" '
363 request = issues_pb2.ListCommentsRequest(
364 parent=self.issue_1_resource_name, page_size=1, filter=filter_str)
365 mc = monorailcontext.MonorailContext(
366 self.services, cnxn=self.cnxn, requester=self.owner.email)
367 with self.assertRaises(exceptions.InputException):
368 self.CallWrapped(self.issues_svcr.ListComments, mc, request)
369
370 def testListComments_UnknownApprovalInFilter(self):
371 """Filter with unknown approval returns no error and no comments."""
372 approval_comment = tracker_pb2.IssueComment(
373 id=123,
374 issue_id=self.issue_1.issue_id,
375 project_id=self.issue_1.project_id,
376 user_id=self.owner.user_id,
377 content='comment 2 - approval 1',
378 approval_id=1)
379 self.services.issue.TestAddComment(approval_comment, self.issue_1.local_id)
380 request = issues_pb2.ListCommentsRequest(
381 parent=self.issue_1_resource_name, page_size=1,
382 filter='approval = "projects/chicken/approvalDefs/404"')
383 mc = monorailcontext.MonorailContext(
384 self.services, cnxn=self.cnxn, requester=self.owner.email)
385 response = self.CallWrapped(self.issues_svcr.ListComments, mc, request)
386 self.assertEqual(len(response.comments), 0)
387
388 def testListComments_ApprovalInFilter(self):
389 approval_comment = tracker_pb2.IssueComment(
390 id=123,
391 issue_id=self.issue_1.issue_id,
392 project_id=self.issue_1.project_id,
393 user_id=self.owner.user_id,
394 content='comment 2 - approval 1',
395 approval_id=1)
396 self.services.issue.TestAddComment(approval_comment, self.issue_1.local_id)
397 request = issues_pb2.ListCommentsRequest(
398 parent=self.issue_1_resource_name, page_size=1,
399 filter='approval = "projects/chicken/approvalDefs/1"')
400 mc = monorailcontext.MonorailContext(
401 self.services, cnxn=self.cnxn, requester=self.owner.email)
402 response = self.CallWrapped(self.issues_svcr.ListComments, mc, request)
403 self.assertEqual(len(response.comments), 1)
404 self.assertEqual(response.comments[0].content, approval_comment.content)
405
406 def testListApprovalValues(self):
407 config = fake.MakeTestConfig(self.project_2.project_id, [], [])
408 self.services.config.StoreConfig(self.cnxn, config)
409
410 # Make regular field def and value
411 fd_1 = fake.MakeTestFieldDef(
412 1, self.project_2.project_id, tracker_pb2.FieldTypes.STR_TYPE,
413 field_name='field1')
414 self.services.config.TestAddFieldDef(fd_1)
415 fv_1 = fake.MakeFieldValue(
416 field_id=fd_1.field_id, str_value='value1', derived=False)
417
418 # Make testing approval def and its associated field def
419 approval_gate = fake.MakeTestFieldDef(
420 2, self.project_2.project_id, tracker_pb2.FieldTypes.APPROVAL_TYPE,
421 field_name='approval-gate-1')
422 self.services.config.TestAddFieldDef(approval_gate)
423 ad = fake.MakeTestApprovalDef(2, approver_ids=[self.user_2.user_id])
424 self.services.config.TestAddApprovalDef(ad, self.project_2.project_id)
425
426 # Make approval value
427 av = fake.MakeApprovalValue(2, set_on=self.PAST_TIME,
428 approver_ids=[self.user_2.user_id], setter_id=self.user_2.user_id)
429
430 # Make field def that belongs to above approval_def
431 fd_2 = fake.MakeTestFieldDef(
432 3, self.project_2.project_id, tracker_pb2.FieldTypes.STR_TYPE,
433 field_name='field2', approval_id=2)
434 self.services.config.TestAddFieldDef(fd_2)
435 fv_2 = fake.MakeFieldValue(
436 field_id=fd_2.field_id, str_value='value2', derived=False)
437
438 issue_resource_name = 'projects/cow/issues/1237'
439 issue = fake.MakeTestIssue(
440 self.project_2.project_id,
441 1237,
442 'sum',
443 'New',
444 self.user_2.user_id,
445 project_name=self.project_2.project_name,
446 field_values=[fv_1, fv_2],
447 approval_values=[av])
448 self.services.issue.TestAddIssue(issue)
449
450 request = issues_pb2.ListApprovalValuesRequest(parent=issue_resource_name)
451 mc = monorailcontext.MonorailContext(
452 self.services, cnxn=self.cnxn, requester=self.owner.email)
453 actual_response = self.CallWrapped(
454 self.issues_svcr.ListApprovalValues, mc, request)
455
456 self.assertEqual(len(actual_response.approval_values), 1)
457 expected_fv = issue_objects_pb2.FieldValue(
458 field='projects/cow/fieldDefs/3',
459 value='value2',
460 derivation=issue_objects_pb2.Derivation.Value('EXPLICIT'))
461 expected = issue_objects_pb2.ApprovalValue(
462 name='projects/cow/issues/1237/approvalValues/2',
463 status=issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NOT_SET'),
464 approvers=['users/222'],
465 approval_def='projects/cow/approvalDefs/2',
466 set_time=timestamp_pb2.Timestamp(seconds=self.PAST_TIME),
467 setter='users/222',
468 field_values=[expected_fv])
469 self.assertEqual(actual_response.approval_values[0], expected)
470
471 def testListApprovalValues_Empty(self):
472 request = issues_pb2.ListApprovalValuesRequest(
473 parent=self.issue_1_resource_name)
474 mc = monorailcontext.MonorailContext(
475 self.services, cnxn=self.cnxn, requester=self.owner.email)
476 actual_response = self.CallWrapped(
477 self.issues_svcr.ListApprovalValues, mc, request)
478 self.assertEqual(len(actual_response.approval_values), 0)
479
480 @mock.patch(
481 'features.send_notifications.PrepareAndSendIssueChangeNotification')
482 def testMakeIssue(self, _fake_pasicn):
483 request_issue = issue_objects_pb2.Issue(
484 summary='sum',
485 status=issue_objects_pb2.Issue.StatusValue(status='New'),
486 cc_users=[issue_objects_pb2.Issue.UserValue(user='users/222')],
487 labels=[issue_objects_pb2.Issue.LabelValue(label='foo-bar')]
488 )
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200489
Copybara854996b2021-09-07 19:36:02 +0000490 request = issues_pb2.MakeIssueRequest(
491 parent='projects/chicken',
492 issue=request_issue,
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200493 description='description',
494 uploads=[issues_pb2.AttachmentUpload(
495 filename='mowgli.gif', content='cute dog')],
Copybara854996b2021-09-07 19:36:02 +0000496 )
497 mc = monorailcontext.MonorailContext(
498 self.services, cnxn=self.cnxn, requester=self.owner.email)
499 response = self.CallWrapped(
500 self.issues_svcr.MakeIssue, mc, request)
501 self.assertEqual(response.summary, 'sum')
502 self.assertEqual(response.status.status, 'New')
503 self.assertEqual(response.cc_users[0].user, 'users/222')
504 self.assertEqual(response.labels[0].label, 'foo-bar')
505 self.assertEqual(response.star_count, 1)
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +0200506 self.assertEqual(response.attachment_count, 1)
507
508 unValid_request = issues_pb2.MakeIssueRequest(
509 parent='projects/chicken',
510 issue=request_issue,
511 description='description',
512 uploads=[issues_pb2.AttachmentUpload(
513 filename='mowgli.gif')],
514 )
515 with self.assertRaisesRegexp(
516 exceptions.InputException,
517 'Uploaded atachment missing filename or content'):
518 self.CallWrapped(self.issues_svcr.MakeIssue, mc, unValid_request)
519
Copybara854996b2021-09-07 19:36:02 +0000520
521 @mock.patch(
522 'features.send_notifications.PrepareAndSendIssueChangeNotification')
523 @mock.patch('time.time')
524 def testModifyIssues(self, fake_time, fake_notify):
525 fake_time.return_value = 12345
526
527 issue = _Issue(780, 1)
528 self.services.project.TestAddProject(
529 issue.project_name, project_id=issue.project_id,
530 owner_ids=[self.owner.user_id])
531
532 issue.labels = ['keep-me', 'remove-me']
533 self.services.issue.TestAddIssue(issue)
534 exp_issue = copy.deepcopy(issue)
535
536 self.services.issue.CreateIssueComment = mock.Mock()
537 mc = monorailcontext.MonorailContext(
538 self.services, cnxn=self.cnxn, requester=self.owner.email)
539
540 request = issues_pb2.ModifyIssuesRequest(
541 deltas=[
542 issues_pb2.IssueDelta(
543 issue=issue_objects_pb2.Issue(
544 name='projects/proj-780/issues/1',
545 labels=[issue_objects_pb2.Issue.LabelValue(
546 label='add-me')]),
547 update_mask=field_mask_pb2.FieldMask(paths=['labels']),
548 labels_remove=['remove-me'])],
549 uploads=[issues_pb2.AttachmentUpload(
550 filename='mowgli.gif', content='cute dog')],
551 comment_content='Release the chicken.',
552 notify_type=issues_pb2.NotifyType.Value('NO_NOTIFICATION'))
553
554 response = self.CallWrapped(
555 self.issues_svcr.ModifyIssues, mc, request)
556 exp_issue.labels = ['keep-me', 'add-me']
557 exp_issue.modified_timestamp = 12345
558 exp_api_issue = self.issues_svcr.converter.ConvertIssue(exp_issue)
559 self.assertEqual([iss for iss in response.issues], [exp_api_issue])
560
561 # All updated issues should have been fetched from DB, skipping cache.
562 # So we expect assume_stale=False was applied to all issues during the
563 # the fetch.
564 exp_issue.assume_stale = False
565 # These derived values get set to the following when an issue goes through
566 # the ApplyFilterRules path. (see filter_helpers._ComputeDerivedFields)
567 exp_issue.derived_owner_id = 0
568 exp_issue.derived_status = ''
569 exp_attachments = [framework_helpers.AttachmentUpload(
570 'mowgli.gif', 'cute dog', 'image/gif')]
571 exp_amendments = [tracker_pb2.Amendment(
572 field=tracker_pb2.FieldID.LABELS, newvalue='-remove-me add-me')]
573 self.services.issue.CreateIssueComment.assert_called_once_with(
574 self.cnxn, exp_issue, mc.auth.user_id, 'Release the chicken.',
575 attachments=exp_attachments, amendments=exp_amendments, commit=False)
576 fake_notify.assert_called_once_with(
577 issue.issue_id, 'testing-app.appspot.com', self.owner.user_id,
578 comment_id=mock.ANY, old_owner_id=None, send_email=False)
579
580 def testModifyIssues_Empty(self):
581 mc = monorailcontext.MonorailContext(
582 self.services, cnxn=self.cnxn, requester=self.owner.email)
583 request = issues_pb2.ModifyIssuesRequest()
584 response = self.CallWrapped(self.issues_svcr.ModifyIssues, mc, request)
585 self.assertEqual(response, issues_pb2.ModifyIssuesResponse())
586
587 @mock.patch('api.v3.api_constants.MAX_MODIFY_ISSUES', 2)
588 @mock.patch('api.v3.api_constants.MAX_MODIFY_IMPACTED_ISSUES', 4)
589 def testModifyIssues_TooMany(self):
590 mc = monorailcontext.MonorailContext(
591 self.services, cnxn=self.cnxn, requester=self.owner.email)
592 request = issues_pb2.ModifyIssuesRequest(
593 deltas=[
594 issues_pb2.IssueDelta(),
595 issues_pb2.IssueDelta(),
596 issues_pb2.IssueDelta()
597 ])
598 with self.assertRaisesRegexp(
599 exceptions.InputException,
600 'Requesting 3 updates when the allowed maximum is 2 updates.'):
601 self.CallWrapped(self.issues_svcr.ModifyIssues, mc, request)
602
603 issue_ref_list = [issue_objects_pb2.IssueRef()]
604 request = issues_pb2.ModifyIssuesRequest(
605 deltas=[
606 issues_pb2.IssueDelta(
607 issue=issue_objects_pb2.Issue(
608 blocked_on_issue_refs=issue_ref_list),
609 blocked_on_issues_remove=issue_ref_list,
610 update_mask=field_mask_pb2.FieldMask(
611 paths=['merged_into_issue_ref'])),
612 issues_pb2.IssueDelta(
613 issue=issue_objects_pb2.Issue(
614 blocking_issue_refs=issue_ref_list),
615 blocking_issues_remove=issue_ref_list)
616 ])
617 with self.assertRaisesRegexp(
618 exceptions.InputException,
619 'Updates include 5 impacted issues when the allowed maximum is 4.'):
620 self.CallWrapped(self.issues_svcr.ModifyIssues, mc, request)
621
622 @mock.patch('time.time', mock.MagicMock(return_value=CURRENT_TIME))
623 @mock.patch(
624 'features.send_notifications.PrepareAndSendApprovalChangeNotification')
625 def testModifyIssueApprovalValues(self, fake_notify):
626 self.services.issue.DeltaUpdateIssueApproval = mock.Mock()
627 config = fake.MakeTestConfig(self.project_1.project_id, [], [])
628 self.services.config.StoreConfig(self.cnxn, config)
629
630 # Make testing approval def and its associated field def
631 field_id = 2
632 approval_field_def = fake.MakeTestFieldDef(
633 field_id,
634 self.project_1.project_id,
635 tracker_pb2.FieldTypes.APPROVAL_TYPE,
636 field_name='approval-gate-1')
637 self.services.config.TestAddFieldDef(approval_field_def)
638 ad = fake.MakeTestApprovalDef(field_id, approver_ids=[self.owner.user_id])
639 self.services.config.TestAddApprovalDef(ad, self.project_1.project_id)
640
641 # Make approval value
642 av = fake.MakeApprovalValue(
643 field_id,
644 status=tracker_pb2.ApprovalStatus.NEEDS_REVIEW,
645 set_on=self.PAST_TIME,
646 approver_ids=[self.owner.user_id],
647 setter_id=self.user_2.user_id)
648
649 issue = fake.MakeTestIssue(
650 self.project_1.project_id,
651 1237,
652 'sum',
653 'New',
654 self.owner.user_id,
655 project_name=self.project_1.project_name,
656 approval_values=[av])
657 self.services.issue.TestAddIssue(issue)
658
659 av_name = 'projects/%s/issues/%d/approvalValues/%d' % (
660 self.project_1.project_name, issue.local_id, ad.approval_id)
661 delta = issues_pb2.ApprovalDelta(
662 approval_value=issue_objects_pb2.ApprovalValue(
663 name=av_name,
664 status=issue_objects_pb2.ApprovalValue.ApprovalStatus.Value('NA')),
665 update_mask=field_mask_pb2.FieldMask(paths=['status']))
666
667 request = issues_pb2.ModifyIssueApprovalValuesRequest(deltas=[delta],)
668 mc = monorailcontext.MonorailContext(
669 self.services, cnxn=self.cnxn, requester=self.owner.email)
670 response = self.CallWrapped(
671 self.issues_svcr.ModifyIssueApprovalValues, mc, request)
672 expected_ingested_delta = tracker_pb2.ApprovalDelta(
673 status=tracker_pb2.ApprovalStatus.NA,
674 set_on=int(CURRENT_TIME),
675 setter_id=self.owner.user_id,
676 )
677 # NOTE: Because we mock out DeltaUpdateIssueApproval, the ApprovalValues
678 # returned haven't been changed in this test. We can't test that it was
679 # changed correctly, but we can make sure it's for the right ApprovalValue.
680 self.assertEqual(len(response.approval_values), 1)
681 self.assertEqual(response.approval_values[0].name, av_name)
682 self.services.issue.DeltaUpdateIssueApproval.assert_called_once_with(
683 mc.cnxn,
684 self.owner.user_id,
685 config,
686 issue,
687 av,
688 expected_ingested_delta,
689 comment_content=u'',
690 is_description=False,
691 attachments=None,
692 kept_attachments=None)
693 fake_notify.assert_called_once_with(
694 issue.issue_id,
695 ad.approval_id,
696 'testing-app.appspot.com',
697 mock.ANY,
698 send_email=True)
699
700 @mock.patch('api.v3.api_constants.MAX_MODIFY_APPROVAL_VALUES', 2)
701 def testModifyIssueApprovalValues_TooMany(self):
702 mc = monorailcontext.MonorailContext(
703 self.services, cnxn=self.cnxn, requester=self.owner.email)
704 request = issues_pb2.ModifyIssueApprovalValuesRequest(
705 deltas=[
706 issues_pb2.ApprovalDelta(),
707 issues_pb2.ApprovalDelta(),
708 issues_pb2.ApprovalDelta()
709 ])
710 with self.assertRaises(exceptions.InputException):
711 self.CallWrapped(self.issues_svcr.ModifyIssueApprovalValues, mc, request)
712
713 def testModifyIssueApprovalValues_Empty(self):
714 request = issues_pb2.ModifyIssueApprovalValuesRequest()
715 mc = monorailcontext.MonorailContext(
716 self.services, cnxn=self.cnxn, requester=self.owner.email)
717 response = self.CallWrapped(
718 self.issues_svcr.ModifyIssueApprovalValues, mc, request)
719 self.assertEqual(len(response.approval_values), 0)
720
721 @mock.patch(
722 'businesslogic.work_env.WorkEnv.GetIssue',
723 return_value=tracker_pb2.Issue(
724 owner_id=0,
725 project_name='chicken',
726 project_id=789,
727 local_id=1234,
728 issue_id=80134))
729 def testModifyCommentState(self, mocked_get_issue):
730 name = self.issue_1_resource_name + '/comments/1'
731 state = issue_objects_pb2.IssueContentState.Value('DELETED')
732 request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
733 mc = monorailcontext.MonorailContext(
734 self.services, cnxn=self.cnxn, requester=self.owner.email)
735 with self.assertRaises(exceptions.NoSuchCommentException):
736 self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
737 mocked_get_issue.assert_any_call(self.issue_1.issue_id, use_cache=False)
738
739 def testModifyCommentState_Delete(self):
740 comment_1 = tracker_pb2.IssueComment(
741 id=124,
742 issue_id=self.issue_1.issue_id,
743 project_id=self.issue_1.project_id,
744 user_id=self.owner.user_id,
745 content='first actual comment')
746 self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
747
748 name = self.issue_1_resource_name + '/comments/1'
749 state = issue_objects_pb2.IssueContentState.Value('DELETED')
750 request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
751 mc = monorailcontext.MonorailContext(
752 self.services, cnxn=self.cnxn, requester=self.owner.email)
753 response = self.CallWrapped(
754 self.issues_svcr.ModifyCommentState, mc, request)
755 self.assertEqual(response.comment.state, state)
756 self.assertEqual(response.comment.content, 'first actual comment')
757
758 # Test noop
759 response = self.CallWrapped(
760 self.issues_svcr.ModifyCommentState, mc, request)
761 self.assertEqual(response.comment.state, state)
762
763 # Test undelete
764 state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
765 request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
766 mc = monorailcontext.MonorailContext(
767 self.services, cnxn=self.cnxn, requester=self.owner.email)
768 response = self.CallWrapped(
769 self.issues_svcr.ModifyCommentState, mc, request)
770 self.assertEqual(response.comment.state, state)
771
772 @mock.patch(
773 'framework.permissions.UpdateIssuePermissions',
774 return_value=permissions.ADMIN_PERMISSIONSET)
775 def testModifyCommentState_Spam(self, _mocked):
776 comment_1 = tracker_pb2.IssueComment(
777 id=124,
778 issue_id=self.issue_1.issue_id,
779 project_id=self.issue_1.project_id,
780 user_id=self.owner.user_id,
781 content='first actual comment')
782 self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
783
784 name = self.issue_1_resource_name + '/comments/1'
785 state = issue_objects_pb2.IssueContentState.Value('SPAM')
786 request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
787 mc = monorailcontext.MonorailContext(
788 self.services, cnxn=self.cnxn, requester=self.owner.email)
789 response = self.CallWrapped(
790 self.issues_svcr.ModifyCommentState, mc, request)
791 self.assertEqual(response.comment.state, state)
792
793 # Test noop
794 response = self.CallWrapped(
795 self.issues_svcr.ModifyCommentState, mc, request)
796 self.assertEqual(response.comment.state, state)
797
798 # Test unflag as spam
799 state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
800 request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
801 mc = monorailcontext.MonorailContext(
802 self.services, cnxn=self.cnxn, requester=self.owner.email)
803 response = self.CallWrapped(
804 self.issues_svcr.ModifyCommentState, mc, request)
805 self.assertEqual(response.comment.state, state)
806
807 def testModifyCommentState_Active(self):
808 comment_1 = tracker_pb2.IssueComment(
809 id=124,
810 issue_id=self.issue_1.issue_id,
811 project_id=self.issue_1.project_id,
812 user_id=self.owner.user_id,
813 content='first actual comment')
814 self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
815
816 name = self.issue_1_resource_name + '/comments/1'
817 state = issue_objects_pb2.IssueContentState.Value('ACTIVE')
818 request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
819 mc = monorailcontext.MonorailContext(
820 self.services, cnxn=self.cnxn, requester=self.owner.email)
821 response = self.CallWrapped(
822 self.issues_svcr.ModifyCommentState, mc, request)
823 self.assertEqual(response.comment.state, state)
824
825 def testModifyCommentState_Spam_ActionNotSupported(self):
826 # Cannot transition from deleted to spam
827 comment_1 = tracker_pb2.IssueComment(
828 id=124,
829 issue_id=self.issue_1.issue_id,
830 project_id=self.issue_1.project_id,
831 user_id=self.owner.user_id,
832 content='first actual comment',
833 deleted_by=self.owner.user_id)
834 self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
835
836 name = self.issue_1_resource_name + '/comments/1'
837 state = issue_objects_pb2.IssueContentState.Value('SPAM')
838 request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
839 mc = monorailcontext.MonorailContext(
840 self.services, cnxn=self.cnxn, requester=self.owner.email)
841 with self.assertRaises(exceptions.ActionNotSupported):
842 self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
843
844 def testModifyCommentState_Delete_ActionNotSupported(self):
845 # Cannot transition from spam to deleted
846 comment_1 = tracker_pb2.IssueComment(
847 id=124,
848 issue_id=self.issue_1.issue_id,
849 project_id=self.issue_1.project_id,
850 user_id=self.owner.user_id,
851 content='first actual comment',
852 is_spam=True)
853 self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
854
855 name = self.issue_1_resource_name + '/comments/1'
856 state = issue_objects_pb2.IssueContentState.Value('DELETED')
857 request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
858 mc = monorailcontext.MonorailContext(
859 self.services, cnxn=self.cnxn, requester=self.owner.email)
860 with self.assertRaises(exceptions.ActionNotSupported):
861 self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
862
863 def testModifyCommentState_NoSuchComment(self):
864 name = self.issue_1_resource_name + '/comments/1'
865 state = issue_objects_pb2.IssueContentState.Value('DELETED')
866 request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
867 mc = monorailcontext.MonorailContext(
868 self.services, cnxn=self.cnxn, requester=self.owner.email)
869 with self.assertRaises(exceptions.NoSuchCommentException):
870 self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
871
872 def testModifyCommentState_Delete_PermissionException(self):
873 comment_1 = tracker_pb2.IssueComment(
874 id=124,
875 issue_id=self.issue_1.issue_id,
876 project_id=self.issue_1.project_id,
877 user_id=self.owner.user_id,
878 content='first actual comment')
879 self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
880
881 name = self.issue_1_resource_name + '/comments/1'
882 state = issue_objects_pb2.IssueContentState.Value('DELETED')
883 request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
884 mc = monorailcontext.MonorailContext(
885 self.services, cnxn=self.cnxn, requester=self.user_2.email)
886 with self.assertRaises(permissions.PermissionException):
887 self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)
888
889 @mock.patch(
890 'framework.permissions.UpdateIssuePermissions',
891 return_value=permissions.READ_ONLY_PERMISSIONSET)
892 def testModifyCommentState_Spam_PermissionException(self, _mocked):
893 comment_1 = tracker_pb2.IssueComment(
894 id=124,
895 issue_id=self.issue_1.issue_id,
896 project_id=self.issue_1.project_id,
897 user_id=self.owner.user_id,
898 content='first actual comment')
899 self.services.issue.TestAddComment(comment_1, self.issue_1.local_id)
900
901 name = self.issue_1_resource_name + '/comments/1'
902 state = issue_objects_pb2.IssueContentState.Value('SPAM')
903 request = issues_pb2.ModifyCommentStateRequest(name=name, state=state)
904 mc = monorailcontext.MonorailContext(
905 self.services, cnxn=self.cnxn, requester=self.user_2.email)
906 with self.assertRaises(permissions.PermissionException):
907 self.CallWrapped(self.issues_svcr.ModifyCommentState, mc, request)