blob: 5b87a131dd2dd52ae24f8db8b51fdbca7817170b [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2017 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"""Tests for the WorkEnv class."""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
10import copy
11import logging
Copybara854996b2021-09-07 19:36:02 +000012import unittest
13import mock
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010014import six
15import sys
Copybara854996b2021-09-07 19:36:02 +000016
17from google.appengine.api import memcache
18from google.appengine.ext import testbed
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010019import pytest
Copybara854996b2021-09-07 19:36:02 +000020
21import settings
22from businesslogic import work_env
23from features import filterrules_helpers
24from framework import authdata
25from framework import exceptions
26from framework import framework_constants
27from framework import framework_helpers
28from framework import framework_views
29from framework import permissions
30from framework import sorting
31from features import send_notifications
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010032from mrproto import features_pb2
33from mrproto import project_pb2
34from mrproto import tracker_pb2
35from mrproto import user_pb2
Copybara854996b2021-09-07 19:36:02 +000036from services import config_svc
37from services import features_svc
38from services import issue_svc
39from services import project_svc
40from services import user_svc
41from services import usergroup_svc
42from services import service_manager
43from services import spam_svc
44from services import star_svc
45from services import template_svc
46from testing import fake
47from testing import testing_helpers
48from tracker import tracker_bizobj
49from tracker import tracker_constants
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010050from redirect import redirectissue
51from api.api_proto import common_pb2
Copybara854996b2021-09-07 19:36:02 +000052
53
54def _Issue(project_id, local_id):
55 # TODO(crbug.com/monorail/8124): Many parts of monorail's codebase
56 # assumes issue.owner_id could never be None and that issues without
57 # owners have owner_id = 0.
58 issue = tracker_pb2.Issue(owner_id=0)
59 issue.project_name = 'proj-%d' % project_id
60 issue.project_id = project_id
61 issue.local_id = local_id
62 issue.issue_id = project_id*100 + local_id
63 return issue
64
65
66class WorkEnvTest(unittest.TestCase):
67
68 def setUp(self):
69 self.cnxn = 'fake connection'
70 self.services = service_manager.Services(
71 config=fake.ConfigService(),
72 cache_manager=fake.CacheManager(),
73 issue=fake.IssueService(),
74 user=fake.UserService(),
75 project=fake.ProjectService(),
76 issue_star=fake.IssueStarService(),
77 project_star=fake.ProjectStarService(),
78 user_star=fake.UserStarService(),
79 hotlist_star=fake.HotlistStarService(),
80 features=fake.FeaturesService(),
81 usergroup=fake.UserGroupService(),
82 template=mock.Mock(spec=template_svc.TemplateService),
83 spam=fake.SpamService())
84 self.project = self.services.project.TestAddProject(
85 'proj', project_id=789, committer_ids=[111])
86 self.component_id_1 = self.services.config.CreateComponentDef(
87 self.cnxn, self.project.project_id, 'Component', 'Docstring', False, [],
88 [], 0, 111, [])
89 self.component_id_2 = self.services.config.CreateComponentDef(
90 self.cnxn, self.project.project_id, 'Component>Test', 'Docstring',
91 False, [], [], 0, 111, [])
92
93 config = fake.MakeTestConfig(self.project.project_id, [], [])
94 config.well_known_statuses = [
95 tracker_pb2.StatusDef(status='Fixed', means_open=False)
96 ]
97 self.services.config.StoreConfig(self.cnxn, config)
98 self.admin_user = self.services.user.TestAddUser(
99 'admin@example.com', 444)
100 self.admin_user.is_site_admin = True
101 self.user_1 = self.services.user.TestAddUser('user_111@example.com', 111)
102 self.user_2 = self.services.user.TestAddUser('user_222@example.com', 222)
103 self.user_3 = self.services.user.TestAddUser('user_333@example.com', 333)
104 self.hotlist = self.services.features.TestAddHotlist(
105 'myhotlist', summary='old sum', owner_ids=[self.user_1.user_id],
106 editor_ids=[self.user_2.user_id], description='old desc',
107 is_private=True)
108 # reserved for testing that a hotlist does not exist
109 self.dne_hotlist_id = 1234
110 self.mr = testing_helpers.MakeMonorailRequest(project=self.project)
111 self.mr.perms = permissions.READ_ONLY_PERMISSIONSET
112 self.field_def_1_name = 'test_field_1'
113 self.field_def_1 = fake.MakeTestFieldDef(
114 101, self.project.project_id, tracker_pb2.FieldTypes.INT_TYPE,
115 field_name=self.field_def_1_name, max_value=10)
116 self.services.config.TestAddFieldDef(self.field_def_1)
117 self.PAST_TIME = 12345
118 self.dne_project_id = 999
119 sorting.InitializeArtValues(self.services)
120
121 self.work_env = work_env.WorkEnv(
122 self.mr, self.services, 'Testing phase')
123
124 def SignIn(self, user_id=111):
125 self.mr.auth = authdata.AuthData.FromUserID(
126 self.cnxn, user_id, self.services)
127 self.mr.perms = permissions.GetPermissions(
128 self.mr.auth.user_pb, self.mr.auth.effective_ids, self.project)
129
130 def testAssertUserCanModifyIssues_Empty(self):
131 with self.work_env as we:
132 we._AssertUserCanModifyIssues([], True)
133
134 def testAssertUserCanModifyIssues_RestrictedFields(self):
135 restricted_int_fd = fake.MakeTestFieldDef(
136 1, 789, tracker_pb2.FieldTypes.INT_TYPE,
137 field_name='int_field', is_restricted_field=True)
138 self.services.config.TestAddFieldDef(restricted_int_fd)
139
140 restricted_enum_fd = fake.MakeTestFieldDef(
141 2, 789, tracker_pb2.FieldTypes.ENUM_TYPE,
142 field_name='enum_field',
143 is_restricted_field=True)
144 self.services.config.TestAddFieldDef(restricted_enum_fd)
145
146 issue = fake.MakeTestIssue(
147 789, 1, 'summary', 'Available', self.admin_user.user_id)
148 self.services.issue.TestAddIssue(issue)
149 delta = tracker_pb2.IssueDelta(
150 summary='changing summary',
151 fields_clear=[restricted_int_fd.field_id],
152 labels_remove=['enum_field-test'])
153 issue_delta_pairs = [(issue, delta)]
154
155 self.SignIn(user_id=self.user_1.user_id)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100156 with self.assertRaisesRegex(permissions.PermissionException,
157 r'.+int_field\n.+enum_field'):
Copybara854996b2021-09-07 19:36:02 +0000158 with self.work_env as we:
159 we._AssertUserCanModifyIssues(issue_delta_pairs, True)
160
161 # Add user_1 as an editor
162 restricted_int_fd.editor_ids = [self.user_1.user_id]
163 restricted_enum_fd.editor_ids = [self.user_1.user_id]
164 with self.work_env as we:
165 we._AssertUserCanModifyIssues(issue_delta_pairs, True)
166
167 def testAssertUserCanModifyIssues_HasEditPerms(self):
168 issue = fake.MakeTestIssue(
169 789, 1, 'summary', 'Available', self.admin_user.user_id)
170 self.services.issue.TestAddIssue(issue)
171 delta = tracker_pb2.IssueDelta(summary='changing summary', cc_ids_add=[111])
172 issue_delta_pairs = [(issue, delta)]
173
174 # Committer can edit issues.
175 self.SignIn(user_id=self.user_1.user_id)
176 with self.work_env as we:
177 we._AssertUserCanModifyIssues(
178 issue_delta_pairs, True, comment_content='ping')
179
180 def testAssertUserCanModifyIssues_MergedInto(self):
181 issue = fake.MakeTestIssue(
182 789, 1, 'summary', 'Available', self.admin_user.user_id)
183 self.services.issue.TestAddIssue(issue)
184
185 restricted_issue = fake.MakeTestIssue(
186 789, 2, 'summary', 'Aavailable', self.admin_user.user_id,
187 labels=['Restrict-View-Chicken'])
188 self.services.issue.TestAddIssue(restricted_issue)
189
190 issue_delta_pairs = [
191 (issue, tracker_pb2.IssueDelta(merged_into=restricted_issue.issue_id))
192 ]
193
194 # Committer cannot merge into issue they cannot edit.
195 self.SignIn(user_id=self.user_1.user_id)
196 with self.assertRaises(permissions.PermissionException):
197 with self.work_env as we:
198 we._AssertUserCanModifyIssues(
199 issue_delta_pairs, True, comment_content='ping')
200
201 def testAssertUserCanModifyIssues_HasFineGrainedPerms(self):
202 self.services.project.TestAddProject(
203 'projWithExtraPerms',
204 project_id=788,
205 contrib_ids=[self.user_1.user_id],
206 extra_perms=[
207 project_pb2.Project.ExtraPerms(
208 member_id=self.user_1.user_id,
209 perms=[
210 permissions.ADD_ISSUE_COMMENT,
211 permissions.EDIT_ISSUE_SUMMARY, permissions.EDIT_ISSUE_OWNER
212 ])
213 ])
214 error_messages_re = []
215
216 # user_1 can update issue summaries in the project.
217 issue_1 = fake.MakeTestIssue(
218 788, 1, 'summary', 'Available', self.admin_user.user_id,
219 project_name='farm')
220 self.services.issue.TestAddIssue(issue_1)
221 issue_delta_pairs = [(issue_1, tracker_pb2.IssueDelta(summary='bok bok'))]
222
223 # user_1 does not have EDIT_ISSUE_CC perms in project.
224 error_messages_re.append(r'.+changes to issue farm:2')
225 issue_2 = fake.MakeTestIssue(
226 788, 2, 'summary', 'Fixed', self.admin_user.user_id,
227 project_name='farm')
228 self.services.issue.TestAddIssue(issue_2)
229 issue_delta_pairs.append(
230 (issue_2, tracker_pb2.IssueDelta(cc_ids_add=[777])))
231
232 # user_1 does not have EDIT_ISSUE_STATUS perms in project.
233 error_messages_re.append(r'.+changes to issue farm:3')
234 issue_3 = fake.MakeTestIssue(
235 788, 3, 'summary', 'Fixed', self.admin_user.user_id,
236 project_name='farm')
237 self.services.issue.TestAddIssue(issue_3)
238 issue_delta_pairs.append(
239 (issue_3, tracker_pb2.IssueDelta(status='eggsHatching')))
240
241 # user_1 can update issue owners in the project.
242 issue_4 = fake.MakeTestIssue(
243 788, 4, 'summary', 'Fixed', self.admin_user.user_id,
244 project_name='farm')
245 self.services.issue.TestAddIssue(issue_3)
246 issue_delta_pairs.append(
247 (issue_4, tracker_pb2.IssueDelta(owner_id=self.user_2.user_id)))
248
249 self.SignIn(user_id=self.user_1.user_id)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100250 with self.assertRaisesRegex(permissions.PermissionException,
251 '\n'.join(error_messages_re)):
Copybara854996b2021-09-07 19:36:02 +0000252 with self.work_env as we:
253 we._AssertUserCanModifyIssues(
254 issue_delta_pairs, False, comment_content='ping')
255
256 def testAssertUserCanModifyIssues_IssueGrantedPerms(self):
257 """We properly take issue granted permissions into account."""
258 granting_fd = tracker_pb2.FieldDef(
259 field_name='grants_editissue',
260 field_id=1,
261 field_type=tracker_pb2.FieldTypes.USER_TYPE,
262 grants_perm='editissue')
263 config = fake.MakeTestConfig(789, [], [])
264 config.field_defs = [granting_fd]
265 self.services.config.StoreConfig('cnxn', config)
266
267 # we add user_2 to "grants_editissue" field which should grant them
268 # "EditIssue" in this issue.
269 issue = fake.MakeTestIssue(
270 789, 1, 'summary', 'Available', self.admin_user.user_id,
271 field_values=[
272 tracker_pb2.FieldValue(field_id=1, user_id=self.user_2.user_id)
273 ])
274 self.services.issue.TestAddIssue(issue)
275 issue_delta_pairs = [
276 (issue, tracker_pb2.IssueDelta(summary='changing summary'))
277 ]
278
279 self.SignIn(user_id=self.user_2.user_id)
280 with self.work_env as we:
281 we._AssertUserCanModifyIssues(issue_delta_pairs, False)
282
283 self.SignIn(user_id=self.user_3.user_id)
284 with self.assertRaises(permissions.PermissionException):
285 with self.work_env as we:
286 we._AssertUserCanModifyIssues(issue_delta_pairs, False)
287
288
289 # FUTURE: GetSiteReadOnlyState()
290 # FUTURE: SetSiteReadOnlyState()
291 # FUTURE: GetSiteBannerMessage()
292 # FUTURE: SetSiteBannerMessage()
293
294 def testCreateProject_Normal(self):
295 """We can create a project."""
296 self.SignIn(user_id=self.admin_user.user_id)
297 with self.work_env as we:
298 project_id = we.CreateProject(
299 'newproj', [111], [222], [333], 'summary', 'desc')
300 actual = we.GetProject(project_id)
301
302 self.assertEqual('summary', actual.summary)
303 self.assertEqual('desc', actual.description)
304 self.services.template.CreateDefaultProjectTemplates\
305 .assert_called_once_with(self.mr.cnxn, project_id)
306
307 def testCreateProject_AlreadyExists(self):
308 """We can create a project."""
309 self.SignIn(user_id=self.admin_user.user_id)
310 # Project 'proj' is created in setUp().
311 with self.assertRaises(exceptions.ProjectAlreadyExists):
312 with self.work_env as we:
313 we.CreateProject('proj', [111], [222], [333], 'summary', 'desc')
314
315 self.assertFalse(
316 self.services.template.CreateDefaultProjectTemplates.called)
317
318 def testCreateProject_NotAllowed(self):
319 """A user without permissions cannon create a project."""
320 self.SignIn()
321 with self.assertRaises(permissions.PermissionException):
322 with self.work_env as we:
323 we.CreateProject('proj', [111], [222], [333], 'summary', 'desc')
324
325 self.assertFalse(
326 self.services.template.CreateDefaultProjectTemplates.called)
327
328 def testCheckProjectName_OK(self):
329 """We can check a project name."""
330 self.SignIn(user_id=self.admin_user.user_id)
331 with self.work_env as we:
332 self.assertIsNone(we.CheckProjectName('foo'))
333
334 def testCheckProjectName_InvalidProjectName(self):
335 """We can check an invalid project name."""
336 self.SignIn(user_id=self.admin_user.user_id)
337 with self.work_env as we:
338 self.assertIsNotNone(we.CheckProjectName('Foo'))
339
340 def testCheckProjectName_AlreadyExists(self):
341 """There is already a project with that name."""
342 self.SignIn(user_id=self.admin_user.user_id)
343 with self.work_env as we:
344 self.assertIsNotNone(we.CheckProjectName('proj'))
345
346 def testCheckProjectName_NotAllowed(self):
347 """Users that can't create a project shouldn't get any information."""
348 self.SignIn()
349 with self.assertRaises(permissions.PermissionException):
350 with self.work_env as we:
351 we.CheckProjectName('Foo')
352
353 def testCheckComponentName_OK(self):
354 self.SignIn()
355 with self.work_env as we:
356 self.assertIsNone(we.CheckComponentName(
357 self.project.project_id, None, 'Component'))
358
359 def testCheckComponentName_ParentComponentOK(self):
360 self.services.config.CreateComponentDef(
361 self.cnxn, self.project.project_id, 'Component', 'Docstring',
362 False, [], [], 0, 111, [])
363 self.SignIn()
364 with self.work_env as we:
365 self.assertIsNone(we.CheckComponentName(
366 self.project.project_id, 'Component', 'SubComponent'))
367
368 def testCheckComponentName_InvalidComponentName(self):
369 self.SignIn()
370 with self.work_env as we:
371 self.assertIsNotNone(we.CheckComponentName(
372 self.project.project_id, None, 'Component>Foo'))
373
374 def testCheckComponentName_ComponentAlreadyExists(self):
375 self.services.config.CreateComponentDef(
376 self.cnxn, self.project.project_id, 'Component', 'Docstring',
377 False, [], [], 0, 111, [])
378 self.SignIn()
379 with self.work_env as we:
380 self.assertIsNotNone(we.CheckComponentName(
381 self.project.project_id, None, 'Component'))
382
383 def testCheckComponentName_NotAllowedToViewProject(self):
384 self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
385 self.SignIn(333)
386 with self.assertRaises(permissions.PermissionException):
387 with self.work_env as we:
388 we.CheckComponentName(self.project.project_id, None, 'Component')
389
390 def testCheckComponentName_ParentComponentDoesntExist(self):
391 self.SignIn()
392 with self.assertRaises(exceptions.NoSuchComponentException):
393 with self.work_env as we:
394 we.CheckComponentName(
395 self.project.project_id, 'Component', 'SubComponent')
396
397 def testCheckFieldName_OK(self):
398 self.SignIn()
399 with self.work_env as we:
400 self.assertIsNone(we.CheckFieldName(
401 self.project.project_id, 'Field'))
402
403 def testCheckFieldName_InvalidFieldName(self):
404 self.SignIn()
405 with self.work_env as we:
406 self.assertIsNotNone(we.CheckFieldName(
407 self.project.project_id, '**Field**'))
408
409 def testCheckFieldName_FieldAlreadyExists(self):
410 fd = fake.MakeTestFieldDef(
411 1, self.project.project_id, tracker_pb2.FieldTypes.STR_TYPE,
412 field_name='Field')
413 self.services.config.TestAddFieldDef(fd)
414 self.SignIn()
415 with self.work_env as we:
416 self.assertIsNotNone(we.CheckFieldName(
417 self.project.project_id, 'Field'))
418
419 def testCheckFieldName_FieldIsPrefixOfAnother(self):
420 fd = fake.MakeTestFieldDef(
421 1, self.project.project_id, tracker_pb2.FieldTypes.STR_TYPE,
422 field_name='Field-Foo')
423 self.services.config.TestAddFieldDef(fd)
424 self.SignIn()
425 with self.work_env as we:
426 self.assertIsNotNone(we.CheckFieldName(
427 self.project.project_id, 'Field'))
428
429 def testCheckFieldName_AnotherFieldIsPrefix(self):
430 fd = fake.MakeTestFieldDef(
431 1, self.project.project_id, tracker_pb2.FieldTypes.STR_TYPE,
432 field_name='Field')
433 self.services.config.TestAddFieldDef(fd)
434 self.SignIn()
435 with self.work_env as we:
436 self.assertIsNotNone(we.CheckFieldName(
437 self.project.project_id, 'Field-Foo'))
438
439 def testCheckFieldName_ReservedPrefix(self):
440 self.SignIn()
441 with self.work_env as we:
442 self.assertIsNotNone(we.CheckFieldName(
443 self.project.project_id, 'Summary'))
444
445 def testCheckFieldName_ReservedSuffix(self):
446 self.SignIn()
447 with self.work_env as we:
448 self.assertIsNotNone(we.CheckFieldName(
449 self.project.project_id, 'Chicken-ApproveR'))
450
451 def testCheckFieldName_NotAllowedToViewProject(self):
452 self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
453 self.SignIn(user_id=333)
454 with self.assertRaises(permissions.PermissionException):
455 with self.work_env as we:
456 we.CheckFieldName(self.project.project_id, 'Field')
457
458 def testListProjects(self):
459 """We can get the project IDs of projects visible to the current user."""
460 # Project 789 is created in setUp()
461 self.services.project.TestAddProject(
462 'proj2', project_id=2, access=project_pb2.ProjectAccess.MEMBERS_ONLY)
463 self.services.project.TestAddProject('proj3', project_id=3)
464 with self.work_env as we:
465 actual = we.ListProjects()
466
467 self.assertEqual([3, 789], actual)
468
469 @mock.patch('settings.branded_domains',
470 {'proj3': 'branded.com', '*': 'bugs.chromium.org'})
471 def testListProjects_BrandedDomain_NotLive(self):
472 """Branded domains don't affect localhost and demo servers."""
473 # Project 789 is created in setUp()
474 self.services.project.TestAddProject(
475 'proj2', project_id=2, access=project_pb2.ProjectAccess.MEMBERS_ONLY)
476 self.services.project.TestAddProject('proj3', project_id=3)
477
478 with self.work_env as we:
479 actual = we.ListProjects(domain='localhost:8080')
480 self.assertEqual([3, 789], actual)
481
482 actual = we.ListProjects(domain='app-id.appspot.com')
483 self.assertEqual([3, 789], actual)
484
485 @mock.patch('settings.branded_domains',
486 {'proj3': 'branded.com', '*': 'bugs.chromium.org'})
487 def testListProjects_BrandedDomain_LiveSite(self):
488 """Project list only contains projects on the current branded domain."""
489 # Project 789 is created in setUp()
490 self.services.project.TestAddProject(
491 'proj2', project_id=2, access=project_pb2.ProjectAccess.MEMBERS_ONLY)
492 self.services.project.TestAddProject('proj3', project_id=3)
493
494 with self.work_env as we:
495 actual = we.ListProjects(domain='branded.com')
496 self.assertEqual([3], actual)
497
498 actual = we.ListProjects(domain='bugs.chromium.org')
499 self.assertEqual([789], actual)
500
501 def testGetProject_Normal(self):
502 """We can get an existing project by project_id."""
503 with self.work_env as we:
504 actual = we.GetProject(789)
505
506 self.assertEqual(self.project, actual)
507
508 def testGetProject_NoSuchProject(self):
509 """We reject attempts to get a non-existent project."""
510 with self.assertRaises(exceptions.NoSuchProjectException):
511 with self.work_env as we:
512 _actual = we.GetProject(999)
513
514 def testGetProject_NotAllowed(self):
515 """We reject attempts to get a project we don't have permission to."""
516 self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
517 with self.assertRaises(permissions.PermissionException):
518 with self.work_env as we:
519 _actual = we.GetProject(789)
520
521 def testGetProjectByName_Normal(self):
522 """We can get an existing project by project_name."""
523 with self.work_env as we:
524 actual = we.GetProjectByName('proj')
525
526 self.assertEqual(self.project, actual)
527
528 def testGetProjectByName_NoSuchProject(self):
529 """We reject attempts to get a non-existent project."""
530 with self.assertRaises(exceptions.NoSuchProjectException):
531 with self.work_env as we:
532 _actual = we.GetProjectByName('huh-what')
533
534 def testGetProjectByName_NoPermission(self):
535 """We reject attempts to get a project we don't have permissions to."""
536 self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
537 with self.assertRaises(permissions.PermissionException):
538 with self.work_env as we:
539 _actual = we.GetProjectByName('proj')
540
541 def AddUserProjects(self):
542 project_states = {
Copybara854996b2021-09-07 19:36:02 +0000543 'archived': project_pb2.ProjectState.ARCHIVED,
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100544 'live': project_pb2.ProjectState.LIVE,
545 'deletable': project_pb2.ProjectState.DELETABLE
546 }
Copybara854996b2021-09-07 19:36:02 +0000547
548 projects = {}
549 for name, state in project_states.items():
550 projects['owner-'+name] = self.services.project.TestAddProject(
551 'owner-' + name, state=state, owner_ids=[222])
552 projects['committer-'+name] = self.services.project.TestAddProject(
553 'committer-' + name, state=state, committer_ids=[222])
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100554 projects['contributor-' + name] = self.services.project.TestAddProject(
555 'contributor-' + name, state=state, contrib_ids=[222])
Copybara854996b2021-09-07 19:36:02 +0000556
557 projects['members-only'] = self.services.project.TestAddProject(
558 'members-only', owner_ids=[222])
559 projects['members-only'].access = (
560 project_pb2.ProjectAccess.MEMBERS_ONLY)
561
562 return projects
563
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100564 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +0000565 def testGatherProjectMembershipsForUser_OtherUser(self):
566 """We can get the projects in which a user has a role.
567 Member only projects are hidden."""
568 projects = self.AddUserProjects()
569
570 with self.work_env as we:
571 owner, committer, contrib = we.GatherProjectMembershipsForUser(222)
572
573 self.assertEqual([projects['owner-live'].project_id], owner)
574 self.assertEqual([projects['committer-live'].project_id], committer)
575 self.assertEqual([projects['contributor-live'].project_id], contrib)
576
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100577 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +0000578 def testGatherProjectMembershipsForUser_OwnUser(self):
579 """We can get the projects in which the logged in user has a role. """
580 projects = self.AddUserProjects()
581
582 self.SignIn(user_id=222)
583 with self.work_env as we:
584 owner, committer, contrib = we.GatherProjectMembershipsForUser(222)
585
586 self.assertEqual(
587 [
588 projects['members-only'].project_id,
589 projects['owner-live'].project_id
590 ], owner)
591 self.assertEqual([projects['committer-live'].project_id], committer)
592 self.assertEqual([projects['contributor-live'].project_id], contrib)
593
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100594 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +0000595 def testGatherProjectMembershipsForUser_Admin(self):
596 """Admins can see all project roles another user has. """
597 projects = self.AddUserProjects()
598
599 self.SignIn(user_id=444)
600 with self.work_env as we:
601 owner, committer, contrib = we.GatherProjectMembershipsForUser(222)
602
603 self.assertEqual(
604 [
605 projects['members-only'].project_id,
606 projects['owner-live'].project_id
607 ], owner)
608 self.assertEqual([projects['committer-live'].project_id], committer)
609 self.assertEqual([projects['contributor-live'].project_id], contrib)
610
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100611 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +0000612 def testGetUserRolesInAllProjects_OtherUsers(self):
613 """We can get the projects in which the user has a role."""
614 projects = self.AddUserProjects()
615
616 with self.work_env as we:
617 owner, member, contrib = we.GetUserRolesInAllProjects({222})
618
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100619 self.assertCountEqual(
620 [projects['owner-archived'], projects['owner-live']],
621 list(owner.values()))
622 self.assertCountEqual(
623 [projects['committer-archived'], projects['committer-live']],
624 list(member.values()))
625 self.assertCountEqual(
626 [projects['contributor-archived'], projects['contributor-live']],
627 list(contrib.values()))
Copybara854996b2021-09-07 19:36:02 +0000628
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100629 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +0000630 def testGetUserRolesInAllProjects_OwnUser(self):
631 """We can get the projects in which the user has a role."""
632 projects = self.AddUserProjects()
633
634 self.SignIn(user_id=222)
635 with self.work_env as we:
636 owner, member, contrib = we.GetUserRolesInAllProjects({222})
637
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100638 self.assertCountEqual(
639 [
640 projects['members-only'], projects['owner-archived'],
641 projects['owner-live']
642 ], list(owner.values()))
643 self.assertCountEqual(
Copybara854996b2021-09-07 19:36:02 +0000644 [projects['committer-archived'], projects['committer-live']],
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100645 list(member.values()))
646 self.assertCountEqual(
Copybara854996b2021-09-07 19:36:02 +0000647 [projects['contributor-archived'], projects['contributor-live']],
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100648 list(contrib.values()))
Copybara854996b2021-09-07 19:36:02 +0000649
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100650 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +0000651 def testGetUserRolesInAllProjects_Admin(self):
652 """We can get the projects in which the user has a role."""
653 projects = self.AddUserProjects()
654
655 self.SignIn(user_id=444)
656 with self.work_env as we:
657 owner, member, contrib = we.GetUserRolesInAllProjects({222})
658
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100659 self.assertCountEqual(
660 [
661 projects['members-only'], projects['owner-archived'],
662 projects['owner-deletable'], projects['owner-live']
663 ], list(owner.values()))
664 self.assertCountEqual(
665 [
666 projects['committer-archived'], projects['committer-deletable'],
667 projects['committer-live']
668 ], list(member.values()))
669 self.assertCountEqual(
670 [
671 projects['contributor-archived'], projects['contributor-deletable'],
672 projects['contributor-live']
673 ], list(contrib.values()))
Copybara854996b2021-09-07 19:36:02 +0000674
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100675 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +0000676 def testGetUserProjects_OnlyLiveOfOtherUsers(self):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100677 """
678 Regular users should only see permitted projects of other users,
679 regardless of state.
680 """
Copybara854996b2021-09-07 19:36:02 +0000681 projects = self.AddUserProjects()
682
683 self.SignIn()
684 with self.work_env as we:
685 owner, archived, member, contrib = we.GetUserProjects({222})
686
687 self.assertEqual([projects['owner-live']], owner)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100688 self.assertEqual([projects['owner-archived']], archived)
Copybara854996b2021-09-07 19:36:02 +0000689 self.assertEqual([projects['committer-live']], member)
690 self.assertEqual([projects['contributor-live']], contrib)
691
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100692 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +0000693 def testGetUserProjects_AdminSeesAll(self):
694 """Admins should see all projects from other users."""
695 projects = self.AddUserProjects()
696
697 self.SignIn(user_id=444)
698 with self.work_env as we:
699 owner, archived, member, contrib = we.GetUserProjects({222})
700
701 self.assertEqual([projects['members-only'], projects['owner-live']], owner)
702 self.assertEqual([projects['owner-archived']], archived)
703 self.assertEqual([projects['committer-live']], member)
704 self.assertEqual([projects['contributor-live']], contrib)
705
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100706 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +0000707 def testGetUserProjects_UserSeesOwnProjects(self):
708 """Users should see all own projects."""
709 projects = self.AddUserProjects()
710
711 self.SignIn(user_id=222)
712 with self.work_env as we:
713 owner, archived, member, contrib = we.GetUserProjects({222})
714
715 self.assertEqual([projects['members-only'], projects['owner-live']], owner)
716 self.assertEqual([projects['owner-archived']], archived)
717 self.assertEqual([projects['committer-live']], member)
718 self.assertEqual([projects['contributor-live']], contrib)
719
720 def testUpdateProject_Normal(self):
721 """We can update an existing project."""
722 self.SignIn(user_id=self.admin_user.user_id)
723 with self.work_env as we:
724 we.UpdateProject(789, read_only_reason='test reason')
725 project = we.GetProject(789)
726
727 self.assertEqual('test reason', project.read_only_reason)
728
729 def testUpdateProject_NoSuchProject(self):
730 """Updating a nonexistent project raises an exception."""
731 self.SignIn(user_id=self.admin_user.user_id)
732 with self.assertRaises(exceptions.NoSuchProjectException):
733 with self.work_env as we:
734 we.UpdateProject(999, summary='new summary')
735
736 def testDeleteProject_Normal(self):
737 """We can mark an existing project as deletable."""
738 self.SignIn(user_id=self.admin_user.user_id)
739 with self.work_env as we:
740 we.DeleteProject(789)
741
742 self.assertEqual(project_pb2.ProjectState.DELETABLE, self.project.state)
743
744 def testDeleteProject_NoSuchProject(self):
745 """Changing a nonexistent project raises an exception."""
746 self.SignIn(user_id=self.admin_user.user_id)
747 with self.assertRaises(exceptions.NoSuchProjectException):
748 with self.work_env as we:
749 we.DeleteProject(999)
750
751 def testStarProject_Normal(self):
752 """We can star and unstar a project."""
753 self.SignIn()
754 with self.work_env as we:
755 self.assertFalse(we.IsProjectStarred(789))
756 we.StarProject(789, True)
757 self.assertTrue(we.IsProjectStarred(789))
758 we.StarProject(789, False)
759 self.assertFalse(we.IsProjectStarred(789))
760
761 def testStarProject_NoSuchProject(self):
762 """We can't star a nonexistent project."""
763 self.SignIn()
764 with self.assertRaises(exceptions.NoSuchProjectException):
765 with self.work_env as we:
766 we.StarProject(999, True)
767
768 def testStarProject_Anon(self):
769 """Anon user can't star a project."""
770 with self.assertRaises(permissions.PermissionException):
771 with self.work_env as we:
772 we.StarProject(789, True)
773
774 def testIsProjectStarred_Normal(self):
775 """We can check if a project is starred."""
776 # Tested by method testStarProject_Normal().
777 pass
778
779 def testIsProjectStarred_NoProjectSpecified(self):
780 """A project ID must be specified."""
781 with self.work_env as we:
782 with self.assertRaises(exceptions.InputException):
783 self.assertFalse(we.IsProjectStarred(None))
784
785 def testIsProjectStarred_NoSuchProject(self):
786 """We can't check for stars on a nonexistent project."""
787 self.SignIn()
788 with self.assertRaises(exceptions.NoSuchProjectException):
789 with self.work_env as we:
790 we.IsProjectStarred(999)
791
792 def testGetProjectStarCount_Normal(self):
793 """We can count the stars of a project."""
794 self.SignIn()
795 with self.work_env as we:
796 self.assertEqual(0, we.GetProjectStarCount(789))
797 we.StarProject(789, True)
798 self.assertEqual(1, we.GetProjectStarCount(789))
799
800 self.SignIn(user_id=self.admin_user.user_id)
801 with self.work_env as we:
802 we.StarProject(789, True)
803 self.assertEqual(2, we.GetProjectStarCount(789))
804 we.StarProject(789, False)
805 self.assertEqual(1, we.GetProjectStarCount(789))
806
807 def testGetProjectStarCount_NoSuchProject(self):
808 """We can't count stars of a nonexistent project."""
809 self.SignIn()
810 with self.assertRaises(exceptions.NoSuchProjectException):
811 with self.work_env as we:
812 we.GetProjectStarCount(999)
813
814 def testGetProjectStarCount_NoProjectSpecified(self):
815 """A project ID must be specified."""
816 with self.work_env as we:
817 with self.assertRaises(exceptions.InputException):
818 self.assertFalse(we.GetProjectStarCount(None))
819
820 def testListStarredProjects_ViewingSelf(self):
821 """A user can view their own starred projects, if they still have access."""
822 project1 = self.services.project.TestAddProject('proj1', project_id=1)
823 project2 = self.services.project.TestAddProject('proj2', project_id=2)
824 with self.work_env as we:
825 self.SignIn()
826 we.StarProject(project1.project_id, True)
827 we.StarProject(project2.project_id, True)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100828 six.assertCountEqual(self, [project1, project2], we.ListStarredProjects())
Copybara854996b2021-09-07 19:36:02 +0000829 project2.access = project_pb2.ProjectAccess.MEMBERS_ONLY
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100830 six.assertCountEqual(self, [project1], we.ListStarredProjects())
Copybara854996b2021-09-07 19:36:02 +0000831
832 def testListStarredProjects_ViewingOther(self):
833 """A user can view their own starred projects, if they still have access."""
834 project1 = self.services.project.TestAddProject('proj1', project_id=1)
835 project2 = self.services.project.TestAddProject('proj2', project_id=2)
836 with self.work_env as we:
837 self.SignIn(user_id=222)
838 we.StarProject(project1.project_id, True)
839 we.StarProject(project2.project_id, True)
840 self.SignIn(user_id=111)
841 self.assertEqual([], we.ListStarredProjects())
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100842 six.assertCountEqual(
843 self, [project1, project2],
844 we.ListStarredProjects(viewed_user_id=222))
Copybara854996b2021-09-07 19:36:02 +0000845 project2.access = project_pb2.ProjectAccess.MEMBERS_ONLY
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100846 six.assertCountEqual(
847 self, [project1], we.ListStarredProjects(viewed_user_id=222))
Copybara854996b2021-09-07 19:36:02 +0000848
849 def testGetProjectConfig_Normal(self):
850 """We can get an existing config by project_id."""
851 config = fake.MakeTestConfig(789, ['LabelOne'], ['New'])
852 self.services.config.StoreConfig('cnxn', config)
853 with self.work_env as we:
854 actual = we.GetProjectConfig(789)
855
856 self.assertEqual(config, actual)
857
858 def testGetProjectConfig_NoSuchProject(self):
859 """We reject attempts to get a non-existent config."""
860 self.services.config.strict = True
861 with self.assertRaises(exceptions.NoSuchProjectException):
862 with self.work_env as we:
863 _actual = we.GetProjectConfig(self.dne_project_id)
864
865 def testListProjectTemplates_IsMember(self):
866 private_tmpl = tracker_pb2.TemplateDef(name='Chicken', members_only=True)
867 public_tmpl = tracker_pb2.TemplateDef(name='Kale', members_only=False)
868 self.services.template.GetProjectTemplates.return_value = [
869 private_tmpl, public_tmpl]
870
871 self.SignIn() # user 111 is a member of self.project
872
873 with self.work_env as we:
874 actual = we.ListProjectTemplates(self.project.project_id)
875
876 self.assertEqual(actual, [private_tmpl, public_tmpl])
877 self.services.template.GetProjectTemplates.assert_called_once_with(
878 self.mr.cnxn, self.project.project_id)
879
880 def testListProjectTemplates_IsNotMember(self):
881 private_tmpl = tracker_pb2.TemplateDef(name='Chicken', members_only=True)
882 public_tmpl = tracker_pb2.TemplateDef(name='Kale', members_only=False)
883 self.services.template.GetProjectTemplates.return_value = [
884 private_tmpl, public_tmpl]
885
886 with self.work_env as we:
887 actual = we.ListProjectTemplates(self.project.project_id)
888
889 self.assertEqual(actual, [public_tmpl])
890 self.services.template.GetProjectTemplates.assert_called_once_with(
891 self.mr.cnxn, self.project.project_id)
892
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100893 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +0000894 def testListComponentDefs(self):
895 project = self.services.project.TestAddProject(
896 'Greece', owner_ids=[self.user_1.user_id])
897 config = fake.MakeTestConfig(project.project_id, [], [])
898 cd_1 = fake.MakeTestComponentDef(project.project_id, 1, path='Circe')
899 cd_2 = fake.MakeTestComponentDef(project.project_id, 2, path='Achilles')
900 cd_3 = fake.MakeTestComponentDef(project.project_id, 3, path='Patroclus')
901 config.component_defs = [cd_1, cd_2, cd_3]
902 self.services.config.StoreConfig(self.cnxn, config)
903
904 self.SignIn(self.user_1.user_id)
905 with self.work_env as we:
906 actual = we.ListComponentDefs(project.project_id, 10, 1)
907 self.assertEqual(actual, work_env.ListResult([cd_2, cd_3], None))
908
909 def testListComponentDefs_NotFound(self):
910 self.SignIn(self.user_2.user_id)
911
912 with self.assertRaises(exceptions.NoSuchProjectException):
913 with self.work_env as we:
914 we.ListComponentDefs(404, 10, 1)
915
916 project = self.services.project.TestAddProject(
917 'Greece',
918 owner_ids=[self.user_1.user_id],
919 access=project_pb2.ProjectAccess.MEMBERS_ONLY)
920 config = fake.MakeTestConfig(project.project_id, [], [])
921 cd_1 = fake.MakeTestComponentDef(project.project_id, 1, path='Circe')
922 config.component_defs = [cd_1]
923 self.services.config.StoreConfig(self.cnxn, config)
924
925 with self.assertRaises(exceptions.NoSuchProjectException):
926 with self.work_env as we:
927 we.ListComponentDefs(project.project_id, 10, 1)
928
929 def testListComponentDefs_InvalidPaginate(self):
930 with self.assertRaises(exceptions.InputException):
931 with self.work_env as we:
932 we.ListComponentDefs(404, -1, 10)
933
934 with self.assertRaises(exceptions.InputException):
935 with self.work_env as we:
936 we.ListComponentDefs(404, 1, -10)
937
938 @mock.patch('time.time')
939 def testCreateComponentDef(self, fake_time):
940 now = 123
941 fake_time.return_value = now
942 project = self.services.project.TestAddProject(
943 'Music', owner_ids=[self.user_1.user_id])
944 admin = self.services.user.TestAddUser('admin@test.com', 555)
945 self.SignIn(self.user_1.user_id)
946 with self.work_env as we:
947 actual = we.CreateComponentDef(
948 project.project_id, 'hanggai', 'hamtlag', [admin.user_id],
949 [self.user_2.user_id], ['taro', 'mowgli'])
950 self.assertEqual(actual.project_id, project.project_id)
951 self.assertEqual(actual.path, 'hanggai')
952 self.assertEqual(actual.docstring, 'hamtlag')
953 self.assertEqual(actual.admin_ids, [admin.user_id])
954 self.assertEqual(actual.cc_ids, [222])
955 self.assertFalse(actual.deprecated)
956 self.assertEqual(actual.created, now)
957 self.assertEqual(actual.creator_id, self.user_1.user_id)
958 self.assertEqual(
959 actual.label_ids,
960 self.services.config.LookupLabelIDs(
961 self.cnxn, project.project_id, ['taro', 'mowgli']))
962
963 # Test with ancestor.
964 self.SignIn(admin.user_id)
965 with self.work_env as we:
966 actual = we.CreateComponentDef(
967 project.project_id, 'hanggai>band', 'rock band',
968 [self.user_2.user_id], [], [])
969 self.assertEqual(actual.project_id, project.project_id)
970 self.assertEqual(actual.path, 'hanggai>band')
971 self.assertEqual(actual.docstring, 'rock band')
972 self.assertEqual(actual.admin_ids, [self.user_2.user_id])
973 self.assertFalse(actual.deprecated)
974 self.assertEqual(actual.created, now)
975 self.assertEqual(actual.creator_id, admin.user_id)
976
977 def testCreateComponentDef_InvalidUsers(self):
978 project = self.services.project.TestAddProject(
979 'Music', owner_ids=[self.user_1.user_id])
980 self.SignIn(self.user_1.user_id)
981 with self.assertRaises(exceptions.InputException):
982 with self.work_env as we:
983 we.CreateComponentDef(
984 project.project_id, 'hanggai', 'hamtlag', [404], [404], [])
985
986 def testCreateComponentDef_InvalidLeaf(self):
987 project = self.services.project.TestAddProject(
988 'Music', owner_ids=[self.user_1.user_id])
989 self.SignIn(self.user_1.user_id)
990 with self.assertRaises(exceptions.InputException):
991 with self.work_env as we:
992 we.CreateComponentDef(
993 project.project_id, 'music>hanggai.rockband', 'hamtlag', [], [], [])
994
995 def testCreateComponentDef_LeafAlreadyExists(self):
996 project = self.services.project.TestAddProject(
997 'Music', owner_ids=[self.user_1.user_id])
998 self.SignIn(self.user_1.user_id)
999 with self.work_env as we:
1000 we.CreateComponentDef(
1001 project.project_id, 'mowgli', 'favorite things',
1002 [self.user_1.user_id], [], [])
1003 with self.assertRaises(exceptions.ComponentDefAlreadyExists):
1004 with self.work_env as we:
1005 we.CreateComponentDef(
1006 project.project_id, 'mowgli', 'more favorite things', [], [], [])
1007
1008 # Test components with ancestors are also checked correctly
1009 with self.work_env as we:
1010 we.CreateComponentDef(
1011 project.project_id, 'mowgli>food', 'lots of chicken', [], [], [])
1012 with self.assertRaises(exceptions.ComponentDefAlreadyExists):
1013 with self.work_env as we:
1014 we.CreateComponentDef(
1015 project.project_id, 'mowgli>food', 'lots of salmon', [], [], [])
1016
1017 def testCreateComponentDef_AncestorNotFound(self):
1018 project = self.services.project.TestAddProject(
1019 'Music', owner_ids=[self.user_1.user_id])
1020 self.SignIn(self.user_1.user_id)
1021 with self.assertRaises(exceptions.InputException):
1022 with self.work_env as we:
1023 we.CreateComponentDef(
1024 project.project_id, 'mowgli>chicken', 'more favorite things', [],
1025 [], [])
1026
1027 def testCreateComponentDef_PermissionDenied(self):
1028 project = self.services.project.TestAddProject(
1029 'Music', owner_ids=[self.user_1.user_id])
1030 admin = self.services.user.TestAddUser('admin@test.com', 888)
1031 self.SignIn(self.user_1.user_id)
1032 with self.work_env as we:
1033 we.CreateComponentDef(
1034 project.project_id, 'mowgli', 'favorite things', [admin.user_id], [],
1035 [])
1036 we.CreateComponentDef(
1037 project.project_id, 'mowgli>beef', 'favorite things', [], [], [])
1038
1039 user = self.services.user.TestAddUser('user@test.com', 777)
1040 self.SignIn(user.user_id)
1041 with self.assertRaises(permissions.PermissionException):
1042 with self.work_env as we:
1043 we.CreateComponentDef(
1044 project.project_id, 'bambi', 'spring time', [], [], [])
1045 with self.assertRaises(permissions.PermissionException):
1046 with self.work_env as we:
1047 we.CreateComponentDef(
1048 project.project_id, 'mowgli>chicken', 'more favorite things', [],
1049 [], [])
1050 with self.assertRaises(permissions.PermissionException):
1051 with self.work_env as we:
1052 we.CreateComponentDef(
1053 project.project_id, 'mowgli>beef>rice', 'more favorite things', [],
1054 [], [])
1055
1056 def testDeleteComponentDef(self):
1057 project = self.services.project.TestAddProject(
1058 'Achilles', owner_ids=[self.user_1.user_id])
1059 config = fake.MakeTestConfig(project.project_id, [], [])
1060 component_def = fake.MakeTestComponentDef(
1061 project.project_id, 1, path='Chickens>Dickens')
1062 config.component_defs = [component_def]
1063 self.services.config.StoreConfig(self.cnxn, config)
1064
1065 self.SignIn(self.user_1.user_id)
1066 with self.work_env as we:
1067 we.DeleteComponentDef(project.project_id, component_def.component_id)
1068
1069 self.assertEqual(config.component_defs, [])
1070
1071 def testDeleteComponentDef_NotFound(self):
1072 project = self.services.project.TestAddProject(
1073 'Achilles', owner_ids=[self.user_1.user_id])
1074
1075 self.SignIn(self.user_1.user_id)
1076 with self.assertRaises(exceptions.NoSuchComponentException):
1077 with self.work_env as we:
1078 we.DeleteComponentDef(project.project_id, 404)
1079
1080 def testDeleteComponentDef_CannotViewProject(self):
1081 project = self.services.project.TestAddProject(
1082 'Achilles',
1083 owner_ids=[self.user_1.user_id],
1084 access=project_pb2.ProjectAccess.MEMBERS_ONLY)
1085
1086 self.SignIn(self.user_2.user_id)
1087 with self.assertRaises(permissions.PermissionException):
1088 with self.work_env as we:
1089 we.DeleteComponentDef(project.project_id, 404)
1090
1091 def testDeleteComponentDef_SubcomponentFound(self):
1092 project = self.services.project.TestAddProject(
1093 'Achilles', owner_ids=[self.user_1.user_id])
1094 config = fake.MakeTestConfig(project.project_id, [], [])
1095 dickens_comp = fake.MakeTestComponentDef(
1096 project.project_id, 1, path='Chickens>Dickens')
1097 chickens_comp = fake.MakeTestComponentDef(
1098 project.project_id, 2, path='Chickens')
1099 config.component_defs = [chickens_comp, dickens_comp]
1100 self.services.config.StoreConfig(self.cnxn, config)
1101
1102 self.SignIn(self.user_1.user_id)
1103 with self.assertRaises(exceptions.InputException):
1104 with self.work_env as we:
1105 we.DeleteComponentDef(project.project_id, chickens_comp.component_id)
1106
1107 def testDeleteComponentDef_NonComponentAdminsCannotDelete(self):
1108 admin = self.services.user.TestAddUser('circe@test.com', 888)
1109 user = self.services.user.TestAddUser('patroclus@test.com', 999)
1110
1111 project = self.services.project.TestAddProject(
1112 'Achilles', owner_ids=[self.user_1.user_id])
1113 config = fake.MakeTestConfig(project.project_id, [], [])
1114
1115 dickens_comp = fake.MakeTestComponentDef(
1116 project.project_id,
1117 1,
1118 path='Chickens>Dickens',
1119 )
1120 dickens_comp.admin_ids = [admin.user_id]
1121 chickens_comp = fake.MakeTestComponentDef(
1122 project.project_id, 2, path='Chickens')
1123
1124 config.component_defs = [chickens_comp, dickens_comp]
1125 self.services.config.StoreConfig(self.cnxn, config)
1126
1127 self.SignIn(admin.user_id)
1128 with self.work_env as we:
1129 we.DeleteComponentDef(project.project_id, dickens_comp.component_id)
1130
1131 self.SignIn(user.user_id)
1132 with self.assertRaises(permissions.PermissionException):
1133 with self.work_env as we:
1134 we.DeleteComponentDef(project.project_id, chickens_comp.component_id)
1135
1136
1137 # FUTURE: labels, statuses, components, rules, templates, and views.
1138 # FUTURE: project saved queries.
1139 # FUTURE: GetProjectPermissionsForUser()
1140
1141 ### Field methods
1142
1143 # FUTURE: All other field methods.
1144
1145 def testGetFieldDef_Normal(self):
1146 """We can get an existing fielddef by field_id."""
1147 fd = fake.MakeTestFieldDef(
1148 2, self.project.project_id, tracker_pb2.FieldTypes.STR_TYPE,
1149 field_name='Field')
1150 self.services.config.TestAddFieldDef(fd)
1151 config = self.services.config.GetProjectConfig(self.cnxn, 789)
1152
1153 with self.work_env as we:
1154 actual = we.GetFieldDef(fd.field_id, self.project)
1155
1156 self.assertEqual(config.field_defs[1], actual)
1157
1158 def testGetFieldDef_NoSuchFieldDef(self):
1159 """We reject attempts to get a non-existent field."""
1160 with self.assertRaises(exceptions.NoSuchFieldDefException):
1161 with self.work_env as we:
1162 _actual = we.GetFieldDef(999, self.project)
1163
1164 @mock.patch(
1165 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1166 @mock.patch(
1167 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1168 def testCreateIssue_Normal(self, fake_pasicn, fake_pasibn):
1169 """We can create an issue."""
1170 self.SignIn(user_id=111)
1171 approval_values = [tracker_pb2.ApprovalValue(approval_id=23, phase_id=3)]
1172 phases = [tracker_pb2.Phase(name='Canary', phase_id=3)]
1173 with self.work_env as we:
1174 actual_issue, comment = we.CreateIssue(
1175 789,
1176 'sum',
1177 'New',
1178 111, [333], ['Hot'], [], [],
1179 'desc',
1180 phases=phases,
1181 approval_values=approval_values)
1182 self.assertEqual(789, actual_issue.project_id)
1183 self.assertEqual('sum', actual_issue.summary)
1184 self.assertEqual('New', actual_issue.status)
1185 self.assertEqual(111, actual_issue.reporter_id)
1186 self.assertEqual(111, actual_issue.owner_id)
1187 self.assertEqual([333], actual_issue.cc_ids)
1188 self.assertEqual([], actual_issue.field_values)
1189 self.assertEqual([], actual_issue.component_ids)
1190 self.assertEqual(approval_values, actual_issue.approval_values)
1191 self.assertEqual(phases, actual_issue.phases)
1192 self.assertEqual('desc', comment.content)
1193 loaded_comments = self.services.issue.GetCommentsForIssue(
1194 self.cnxn, actual_issue.issue_id)
1195 self.assertEqual('desc', loaded_comments[0].content)
1196
1197 # Verify that an indexing task was enqueued for this issue:
1198 self.assertTrue(self.services.issue.enqueue_issues_called)
1199 self.assertEqual(1, len(self.services.issue.enqueued_issues))
1200 self.assertEqual(actual_issue.issue_id,
1201 self.services.issue.enqueued_issues[0])
1202
1203 # Verify that tasks were queued to send email notifications.
1204 hostport = 'testing-app.appspot.com'
1205 fake_pasicn.assert_called_once_with(
1206 actual_issue.issue_id, hostport, 111, comment_id=comment.id)
1207 fake_pasibn.assert_called_once_with(
1208 actual_issue.issue_id, hostport, [], 111)
1209
1210 @mock.patch(
1211 'settings.preferred_domains', {'testing-app.appspot.com': 'example.com'})
1212 @mock.patch(
1213 'settings.branded_domains', {'proj': 'branded.com'})
1214 @mock.patch(
1215 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1216 @mock.patch(
1217 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1218 def testCreateIssue_Branded(self, fake_pasicn, fake_pasibn):
1219 """Use branded domains in notification about creating an issue."""
1220 self.SignIn(user_id=111)
1221 with self.work_env as we:
1222 actual_issue, comment = we.CreateIssue(
1223 789, 'sum', 'New', 111, [333], ['Hot'], [], [], 'desc')
1224
1225 self.assertEqual('proj', actual_issue.project_name)
1226 # Verify that tasks were queued to send email notifications.
1227 hostport = 'branded.com'
1228 fake_pasicn.assert_called_once_with(
1229 actual_issue.issue_id, hostport, 111, comment_id=comment.id)
1230 fake_pasibn.assert_called_once_with(
1231 actual_issue.issue_id, hostport, [], 111)
1232
1233 @mock.patch(
1234 'settings.preferred_domains', {'testing-app.appspot.com': 'example.com'})
1235 @mock.patch(
1236 'settings.branded_domains', {'other-proj': 'branded.com'})
1237 @mock.patch(
1238 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1239 @mock.patch(
1240 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1241 def testCreateIssue_Nonbranded(self, fake_pasicn, fake_pasibn):
1242 """Don't use branded domains when creating issue in different project."""
1243 self.SignIn(user_id=111)
1244 with self.work_env as we:
1245 actual_issue, comment = we.CreateIssue(
1246 789, 'sum', 'New', 111, [333], ['Hot'], [], [], 'desc')
1247
1248 self.assertEqual('proj', actual_issue.project_name)
1249 # Verify that tasks were queued to send email notifications.
1250 hostport = 'example.com'
1251 fake_pasicn.assert_called_once_with(
1252 actual_issue.issue_id, hostport, 111, comment_id=comment.id)
1253 fake_pasibn.assert_called_once_with(
1254 actual_issue.issue_id, hostport, [], 111)
1255
1256 @mock.patch(
1257 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1258 @mock.patch(
1259 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1260 def testCreateIssue_DontSendEmail(self, fake_pasicn, fake_pasibn):
1261 """We can create an issue, without queueing notification tasks."""
1262 self.SignIn(user_id=111)
1263 with self.work_env as we:
1264 actual_issue, comment = we.CreateIssue(
1265 789,
1266 'sum',
1267 'New',
1268 111, [333], ['Hot'], [], [],
1269 'desc',
1270 send_email=False)
1271 self.assertEqual(789, actual_issue.project_id)
1272 self.assertEqual('sum', actual_issue.summary)
1273 self.assertEqual('New', actual_issue.status)
1274 self.assertEqual('desc', comment.content)
1275
1276 # Verify that tasks were not queued to send email notifications.
1277 self.assertEqual([], fake_pasicn.mock_calls)
1278 self.assertEqual([], fake_pasibn.mock_calls)
1279
1280 @mock.patch(
1281 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1282 @mock.patch(
1283 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1284 def testCreateIssue_ImportedIssue_Allowed(self, _fake_pasicn, _fake_pasibn):
1285 """We can create an imported issue, if the requester has permission."""
1286 PAST_TIME = 123456
1287 self.project.extra_perms = [project_pb2.Project.ExtraPerms(
1288 member_id=111, perms=['ImportComment'])]
1289 self.SignIn(user_id=111)
1290 with self.work_env as we:
1291 actual_issue, comment = we.CreateIssue(
1292 789,
1293 'sum',
1294 'New',
1295 111, [333], ['Hot'], [], [],
1296 'desc',
1297 send_email=False,
1298 reporter_id=222,
1299 timestamp=PAST_TIME)
1300 self.assertEqual(789, actual_issue.project_id)
1301 self.assertEqual('sum', actual_issue.summary)
1302 self.assertEqual(222, actual_issue.reporter_id)
1303 self.assertEqual(PAST_TIME, actual_issue.opened_timestamp)
1304 self.assertEqual(222, comment.user_id)
1305 self.assertEqual(111, comment.importer_id)
1306 self.assertEqual(PAST_TIME, comment.timestamp)
1307
1308 @mock.patch(
1309 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1310 @mock.patch(
1311 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1312 def testCreateIssue_ImportedIssue_Denied(self, _fake_pasicn, _fake_pasibn):
1313 """We can refuse to import an issue, if requester lacks permission."""
1314 PAST_TIME = 123456
1315 # Note: no "ImportComment" permission is granted.
1316 self.SignIn(user_id=111)
1317 with self.assertRaises(permissions.PermissionException):
1318 with self.work_env as we:
1319 we.CreateIssue(
1320 789, 'sum', 'New', 222, [333], ['Hot'], [], [], 'desc',
1321 send_email=False, reporter_id=222, timestamp=PAST_TIME)
1322
1323 @mock.patch(
1324 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1325 @mock.patch(
1326 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1327 def testCreateIssue_OnwerValidation(self, _fake_pasicn, _fake_pasibn):
1328 """We validate the owner."""
1329 self.SignIn(user_id=111)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001330 with self.assertRaisesRegex(exceptions.InputException,
1331 'Issue owner must be a project member'):
Copybara854996b2021-09-07 19:36:02 +00001332 with self.work_env as we:
1333 # user_id 222 is not a project member
1334 we.CreateIssue(789, 'sum', 'New', 222, [333], ['Hot'], [], [], 'desc')
1335
1336 @mock.patch(
1337 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1338 @mock.patch(
1339 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1340 def testCreateIssue_SummaryValidation(self, _fake_pasicn, _fake_pasibn):
1341 """We validate the summary."""
1342 self.SignIn(user_id=111)
1343 with self.assertRaises(exceptions.InputException):
1344 with self.work_env as we:
1345 # Summary cannot be empty
1346 we.CreateIssue(789, '', 'New', 111, [333], ['Hot'], [], [], 'desc')
1347 with self.assertRaises(exceptions.InputException):
1348 with self.work_env as we:
1349 # Summary cannot be only spaces
1350 we.CreateIssue(789, ' ', 'New', 111, [333], ['Hot'], [], [], 'desc')
1351
1352 @mock.patch(
1353 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1354 @mock.patch(
1355 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1356 def testCreateIssue_DescriptionValidation(self, _fake_pasicn, _fake_pasibn):
1357 """We validate the description."""
1358 self.SignIn(user_id=111)
1359 with self.assertRaises(exceptions.InputException):
1360 with self.work_env as we:
1361 # Description cannot be empty
1362 we.CreateIssue(789, 'sum', 'New', 111, [333], ['Hot'], [], [], '')
1363 with self.assertRaises(exceptions.InputException):
1364 with self.work_env as we:
1365 # Description cannot be only spaces
1366 we.CreateIssue(789, 'sum', 'New', 111, [333], ['Hot'], [], [], ' ')
1367
1368 @mock.patch(
1369 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1370 @mock.patch(
1371 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1372 def testCreateIssue_FieldValueValidation(self, _fake_pasicn, _fake_pasibn):
1373 """We validate field values against field definitions."""
1374 self.SignIn(user_id=111)
1375 # field_def_1 has a max of 10.
1376 fv = fake.MakeFieldValue(field_id=self.field_def_1.field_id, int_value=11)
1377 with self.assertRaises(exceptions.InputException):
1378 with self.work_env as we:
1379 we.CreateIssue(789, 'sum', 'New', 111, [], [], [fv], [], '')
1380
1381 @mock.patch(
1382 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1383 @mock.patch(
1384 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1385 def testCreateIssue_AppliesFilterRules(self, _fake_pasicn, _fake_pasibn):
1386 """We apply filter rules."""
1387 self.services.features.TestAddFilterRule(
1388 789, '-has:component', add_labels=['no-component'])
1389
1390 self.SignIn(user_id=111)
1391 with self.work_env as we:
1392 actual_issue, _ = we.CreateIssue(
1393 789, 'sum', 'New', 111, [333], [], [], [], 'desc')
1394 self.assertEqual(len(actual_issue.derived_labels), 1)
1395 self.assertEqual(actual_issue.derived_labels[0], 'no-component')
1396
1397 @mock.patch(
1398 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1399 @mock.patch(
1400 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1401 def testCreateIssue_RaiseFilterErrors(self, _fake_pasicn, _fake_pasibn):
1402 """We raise FilterRuleException if filter rule should show error."""
1403 self.services.features.TestAddFilterRule(789, '-has:component', error='er')
1404 PAST_TIME = 123456
1405 self.SignIn(user_id=111)
1406 with self.assertRaises(exceptions.FilterRuleException):
1407 with self.work_env as we:
1408 we.CreateIssue(
1409 789,
1410 'sum',
1411 'New',
1412 111, [], [], [], [],
1413 'desc',
1414 send_email=False,
1415 timestamp=PAST_TIME)
1416
1417 @mock.patch(
1418 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1419 @mock.patch(
1420 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1421 def testCreateIssue_IgnoresFilterErrors(self, _fake_pasicn, _fake_pasibn):
1422 """We can apply filter rules and ignore resulting errors."""
1423 self.services.features.TestAddFilterRule(789, '-has:component', error='er')
1424 self.SignIn(user_id=111)
1425 with self.work_env as we:
1426 actual_issue, _ = we.CreateIssue(
1427 789,
1428 'sum',
1429 'New',
1430 111, [], [], [], [],
1431 'desc',
1432 send_email=False,
1433 raise_filter_errors=False)
1434 self.assertEqual(len(actual_issue.component_ids), 0)
1435
1436 def testMakeIssueFromDelta(self):
1437 # TODO(crbug/monorail/7197): implement tests
1438 pass
1439
1440 @mock.patch(
1441 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1442 @mock.patch(
1443 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1444 def testMakeIssue_Normal(self, _fake_pasicn, _fake_pasibn):
1445 self.SignIn(user_id=111)
1446 fd_id = self.services.config.CreateFieldDef(
1447 self.cnxn,
1448 self.project.project_id,
1449 'Restricted-Foo',
1450 'STR_TYPE',
1451 None,
1452 None,
1453 None,
1454 None,
1455 None,
1456 None,
1457 None,
1458 None,
1459 None,
1460 None,
1461 None,
1462 None,
1463 None,
1464 None, [], [111],
1465 is_restricted_field=True)
1466 input_fv = tracker_pb2.FieldValue(field_id=fd_id, str_value='Bar')
1467 input_issue = tracker_pb2.Issue(
1468 project_id=789,
1469 owner_id=111,
1470 summary='sum',
1471 status='New',
1472 field_values=[input_fv])
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +02001473 attachments = [
1474 ('README.md', 'readme content', 'text/plain'),
1475 ('hello.txt', 'hello content', 'text/plain')]
Copybara854996b2021-09-07 19:36:02 +00001476 with self.work_env as we:
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +02001477 actual_issue = we.MakeIssue(
1478 input_issue, 'description', False, attachments)
Copybara854996b2021-09-07 19:36:02 +00001479 self.assertEqual(actual_issue.project_id, 789)
1480 self.assertEqual(actual_issue.summary, 'sum')
1481 self.assertEqual(actual_issue.status, 'New')
1482 self.assertEqual(actual_issue.reporter_id, 111)
1483 self.assertEqual(actual_issue.field_values, [input_fv])
Adrià Vilanova Martínezac4a6442022-05-15 19:05:13 +02001484 self.assertEqual(2, actual_issue.attachment_count)
Copybara854996b2021-09-07 19:36:02 +00001485 @mock.patch(
1486 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1487 @mock.patch(
1488 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1489 def testMakeIssue_ChecksRestrictedFields(self, _fake_pasicn, _fake_pasibn):
1490 self.SignIn(user_id=222)
1491 fd_id = self.services.config.CreateFieldDef(
1492 self.cnxn,
1493 self.project.project_id,
1494 'Restricted-Foo',
1495 'STR_TYPE',
1496 None,
1497 None,
1498 None,
1499 None,
1500 None,
1501 None,
1502 None,
1503 None,
1504 None,
1505 None,
1506 None,
1507 None,
1508 None,
1509 None, [], [111],
1510 is_restricted_field=True)
1511 input_fv = tracker_pb2.FieldValue(field_id=fd_id, str_value='Bar')
1512 input_issue = tracker_pb2.Issue(
1513 project_id=789, summary='sum', status='New', field_values=[input_fv])
1514 with self.assertRaises(permissions.PermissionException):
1515 with self.work_env as we:
1516 we.MakeIssue(input_issue, 'description', False)
1517
1518 @mock.patch(
1519 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
1520 @mock.patch(
1521 'features.send_notifications.PrepareAndSendIssueChangeNotification')
1522 def testMakeIssue_ChecksRestrictedLabels(self, _fake_pasicn, _fake_pasibn):
1523 """Also checks restricted field that are masked as labels."""
1524 self.SignIn(user_id=222)
1525 self.services.config.CreateFieldDef(
1526 self.cnxn,
1527 self.project.project_id,
1528 'Rfoo',
1529 'ENUM_TYPE',
1530 None,
1531 None,
1532 None,
1533 None,
1534 None,
1535 None,
1536 None,
1537 None,
1538 None,
1539 None,
1540 None,
1541 None,
1542 None,
1543 None, [], [111],
1544 is_restricted_field=True)
1545 input_issue = tracker_pb2.Issue(
1546 project_id=789, summary='sum', status='New', labels=['Rfoo-bar'])
1547 with self.assertRaises(permissions.PermissionException):
1548 with self.work_env as we:
1549 we.MakeIssue(input_issue, 'description', False)
1550
1551 @mock.patch('services.tracker_fulltext.IndexIssues')
1552 @mock.patch('services.tracker_fulltext.UnindexIssues')
1553 def testMoveIssue_Normal(self, mock_unindex, mock_index):
1554 """We can move issues."""
1555 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1556 self.services.issue.TestAddIssue(issue)
1557 self.project.owner_ids = [111]
1558 target_project = self.services.project.TestAddProject(
1559 'dest', project_id=988, committer_ids=[111])
1560
1561 self.SignIn(user_id=111)
1562 with self.work_env as we:
1563 moved_issue = we.MoveIssue(issue, target_project)
1564
1565 self.assertEqual(moved_issue.project_name, 'dest')
1566 self.assertEqual(moved_issue.local_id, 1)
1567
1568 moved_issue = self.services.issue.GetIssueByLocalID(
1569 'cnxn', target_project.project_id, 1)
1570 self.assertEqual(target_project.project_id, moved_issue.project_id)
1571 self.assertEqual(issue.summary, moved_issue.summary)
1572 self.assertEqual(moved_issue.reporter_id, 111)
1573
1574 mock_unindex.assert_called_once_with([issue.issue_id])
1575 mock_index.assert_called_once_with(
1576 self.mr.cnxn, [issue], self.services.user, self.services.issue,
1577 self.services.config)
1578
1579 @mock.patch('services.tracker_fulltext.IndexIssues')
1580 @mock.patch('services.tracker_fulltext.UnindexIssues')
1581 def testMoveIssue_MoveBackAgain(self, _mock_unindex, _mock_index):
1582 """We can move issues backt and get the old id."""
1583 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1584 issue.project_name = 'proj'
1585 self.services.issue.TestAddIssue(issue)
1586 self.project.owner_ids = [111]
1587 target_project = self.services.project.TestAddProject(
1588 'dest', project_id=988, owner_ids=[111])
1589
1590 self.SignIn(user_id=111)
1591 with self.work_env as we:
1592 moved_issue = we.MoveIssue(issue, target_project)
1593 moved_issue = we.MoveIssue(moved_issue, self.project)
1594
1595 self.assertEqual(moved_issue.project_name, 'proj')
1596 self.assertEqual(moved_issue.local_id, 1)
1597
1598 moved_issue = self.services.issue.GetIssueByLocalID(
1599 'cnxn', self.project.project_id, 1)
1600 self.assertEqual(self.project.project_id, moved_issue.project_id)
1601
1602 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
1603 self.assertEqual(
1604 comments[1].content, 'Moved issue proj:1 to now be issue dest:1.')
1605 self.assertEqual(
1606 comments[2].content, 'Moved issue dest:1 back to issue proj:1 again.')
1607
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001608 @mock.patch('services.tracker_fulltext.IndexIssues')
1609 @mock.patch('services.tracker_fulltext.UnindexIssues')
1610 def testMoveIssue_AllowedRestrictions(self, mock_unindex, mock_index):
1611 """We can move restricted issues on allowed projects and labels."""
1612 issue = fake.MakeTestIssue(
1613 789, 1, 'sum', 'New', 111, issue_id=78901, project_name='WebRTC')
1614 issue.labels = ['Restrict-View-SecurityTeam']
1615 self.services.issue.TestAddIssue(issue)
1616 self.project.owner_ids = [111]
1617 target_project = self.services.project.TestAddProject(
1618 'Chromium', project_id=988, committer_ids=[111])
1619
1620 self.SignIn(user_id=111)
1621 with self.work_env as we:
1622 moved_issue = we.MoveIssue(issue, target_project)
1623
1624 self.assertEqual(moved_issue.project_name, 'Chromium')
1625 self.assertEqual(moved_issue.local_id, 1)
1626
1627 moved_issue = self.services.issue.GetIssueByLocalID(
1628 'cnxn', target_project.project_id, 1)
1629 self.assertEqual(target_project.project_id, moved_issue.project_id)
1630 self.assertEqual(issue.summary, moved_issue.summary)
1631 self.assertEqual(moved_issue.reporter_id, 111)
1632
1633 mock_unindex.assert_called_once_with([issue.issue_id])
1634 mock_index.assert_called_once_with(
1635 self.mr.cnxn, [issue], self.services.user, self.services.issue,
1636 self.services.config)
1637
Copybara854996b2021-09-07 19:36:02 +00001638 def testMoveIssue_Anon(self):
1639 """Anon can't move issues."""
1640 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1641 self.services.issue.TestAddIssue(issue)
1642 target_project = self.services.project.TestAddProject(
1643 'dest', project_id=988)
1644
1645 with self.assertRaises(permissions.PermissionException):
1646 with self.work_env as we:
1647 we.MoveIssue(issue, target_project)
1648
1649 def testMoveIssue_CantDeleteIssue(self):
1650 """We can't move issues if we don't have DeleteIssue perm on the issue."""
1651 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1652 self.services.issue.TestAddIssue(issue)
1653 target_project = self.services.project.TestAddProject(
1654 'dest', project_id=988, committer_ids=[111])
1655
1656 self.SignIn(user_id=111)
1657 with self.assertRaises(permissions.PermissionException):
1658 with self.work_env as we:
1659 we.MoveIssue(issue, target_project)
1660
1661 def testMoveIssue_CantEditIssueOnTargetProject(self):
1662 """We can't move issues if we don't have EditIssue perm on target."""
1663 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1664 self.services.issue.TestAddIssue(issue)
1665 self.project.owner_ids = [111]
1666 target_project = self.services.project.TestAddProject(
1667 'dest', project_id=989)
1668
1669 self.SignIn(user_id=111)
1670 with self.assertRaises(permissions.PermissionException):
1671 with self.work_env as we:
1672 we.MoveIssue(issue, target_project)
1673
1674 def testMoveIssue_CantRestrictions(self):
1675 """We can't move issues if they have restriction labels."""
1676 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1677 issue.labels = ['Restrict-Foo-Bar']
1678 self.services.issue.TestAddIssue(issue)
1679 self.project.owner_ids = [111]
1680 target_project = self.services.project.TestAddProject(
1681 'dest', project_id=989, committer_ids=[111])
1682
1683 self.SignIn(user_id=111)
1684 with self.assertRaises(exceptions.InputException):
1685 with self.work_env as we:
1686 we.MoveIssue(issue, target_project)
1687
1688 def testMoveIssue_TooLongIssue(self):
1689 """We can't move issues if the comment is too long."""
1690 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1691 self.services.issue.TestAddIssue(issue)
1692 target_project = self.services.project.TestAddProject(
1693 'dest', project_id=988, committer_ids=[111])
1694
1695 self.SignIn(user_id=111)
1696 with self.assertRaises(permissions.PermissionException):
1697 with self.work_env as we:
1698 we.MoveIssue(issue, target_project)
1699
1700 @mock.patch('services.tracker_fulltext.IndexIssues')
1701 def testCopyIssue_Normal(self, mock_index):
1702 """We can copy issues."""
1703 issue = fake.MakeTestIssue(
1704 789, 1, 'sum', 'New', 111, issue_id=78901, project_name='proj')
1705 self.services.issue.TestAddIssue(issue)
1706 self.project.owner_ids = [111]
1707 target_project = self.services.project.TestAddProject(
1708 'dest', project_id=988, committer_ids=[111])
1709
1710 self.SignIn(user_id=111)
1711 with self.work_env as we:
1712 copied_issue = we.CopyIssue(issue, target_project)
1713
1714 self.assertEqual(copied_issue.project_name, 'dest')
1715 self.assertEqual(copied_issue.local_id, 1)
1716
1717 # Original issue should still exist.
1718 self.services.issue.GetIssueByLocalID('cnxn', 789, 1)
1719
1720 copied_issue = self.services.issue.GetIssueByLocalID(
1721 'cnxn', target_project.project_id, 1)
1722 self.assertEqual(target_project.project_id, copied_issue.project_id)
1723 self.assertEqual(issue.summary, copied_issue.summary)
1724 self.assertEqual(copied_issue.reporter_id, 111)
1725
1726 mock_index.assert_called_once_with(
1727 self.mr.cnxn, [copied_issue], self.services.user, self.services.issue,
1728 self.services.config)
1729
1730 comment = self.services.issue.GetCommentsForIssue(
1731 'cnxn', copied_issue.issue_id)[-1]
1732 self.assertEqual(1, len(comment.amendments))
1733 amendment = comment.amendments[0]
1734 self.assertEqual(
1735 tracker_pb2.Amendment(
1736 field=tracker_pb2.FieldID.PROJECT,
1737 newvalue='dest',
1738 added_user_ids=[],
1739 removed_user_ids=[]),
1740 amendment)
1741
1742 @mock.patch('services.tracker_fulltext.IndexIssues')
1743 def testCopyIssue_SameProject(self, mock_index):
1744 """We can copy issues."""
1745 issue = fake.MakeTestIssue(
1746 789, 1, 'sum', 'New', 111, issue_id=78901, project_name='proj')
1747 self.services.issue.TestAddIssue(issue)
1748 self.project.owner_ids = [111]
1749 target_project = self.project
1750
1751 self.SignIn(user_id=111)
1752 with self.work_env as we:
1753 copied_issue = we.CopyIssue(issue, target_project)
1754
1755 self.assertEqual(copied_issue.project_name, 'proj')
1756 self.assertEqual(copied_issue.local_id, 2)
1757
1758 # Original issue should still exist.
1759 self.services.issue.GetIssueByLocalID('cnxn', 789, 1)
1760
1761 copied_issue = self.services.issue.GetIssueByLocalID(
1762 'cnxn', target_project.project_id, 2)
1763 self.assertEqual(target_project.project_id, copied_issue.project_id)
1764 self.assertEqual(issue.summary, copied_issue.summary)
1765 self.assertEqual(copied_issue.reporter_id, 111)
1766
1767 mock_index.assert_called_once_with(
1768 self.mr.cnxn, [copied_issue], self.services.user, self.services.issue,
1769 self.services.config)
1770 comment = self.services.issue.GetCommentsForIssue(
1771 'cnxn', copied_issue.issue_id)[-1]
1772 self.assertEqual(0, len(comment.amendments))
1773
1774 def testCopyIssue_Anon(self):
1775 """Anon can't copy issues."""
1776 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1777 self.services.issue.TestAddIssue(issue)
1778 target_project = self.services.project.TestAddProject(
1779 'dest', project_id=988)
1780
1781 with self.assertRaises(permissions.PermissionException):
1782 with self.work_env as we:
1783 we.CopyIssue(issue, target_project)
1784
1785 def testCopyIssue_CantDeleteIssue(self):
1786 """We can't copy issues if we don't have DeleteIssue perm on the issue."""
1787 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1788 self.services.issue.TestAddIssue(issue)
1789 target_project = self.services.project.TestAddProject(
1790 'dest', project_id=988, committer_ids=[111])
1791
1792 self.SignIn(user_id=111)
1793 with self.assertRaises(permissions.PermissionException):
1794 with self.work_env as we:
1795 we.CopyIssue(issue, target_project)
1796
1797 def testCopyIssue_CantEditIssueOnTargetProject(self):
1798 """We can't copy issues if we don't have EditIssue perm on target."""
1799 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1800 self.services.issue.TestAddIssue(issue)
1801 self.project.owner_ids = [111]
1802 target_project = self.services.project.TestAddProject(
1803 'dest', project_id=989)
1804
1805 self.SignIn(user_id=111)
1806 with self.assertRaises(permissions.PermissionException):
1807 with self.work_env as we:
1808 we.CopyIssue(issue, target_project)
1809
1810 def testCopyIssue_CantRestrictions(self):
1811 """We can't copy issues if they have restriction labels."""
1812 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1813 issue.labels = ['Restrict-Foo-Bar']
1814 self.services.issue.TestAddIssue(issue)
1815 self.project.owner_ids = [111]
1816 target_project = self.services.project.TestAddProject(
1817 'dest', project_id=989, committer_ids=[111])
1818
1819 self.SignIn(user_id=111)
1820 with self.assertRaises(exceptions.InputException):
1821 with self.work_env as we:
1822 we.CopyIssue(issue, target_project)
1823
1824 @mock.patch('search.frontendsearchpipeline.FrontendSearchPipeline')
1825 def testSearchIssues(self, mocked_pipeline):
1826 mocked_instance = mocked_pipeline.return_value
1827 mocked_instance.total_count = 10
1828 mocked_instance.visible_results = ['a', 'b']
1829 with self.work_env as we:
1830 actual = we.SearchIssues('', ['proj'], 123, 20, 0, '')
1831 expected = work_env.ListResult(['a', 'b'], None)
1832 self.assertEqual(actual, expected)
1833
1834 @mock.patch('search.frontendsearchpipeline.FrontendSearchPipeline')
1835 def testSearchIssues_paginates(self, mocked_pipeline):
1836 mocked_instance = mocked_pipeline.return_value
1837 mocked_instance.total_count = 50
1838 mocked_instance.visible_results = ['a', 'b']
1839 with self.work_env as we:
1840 actual = we.SearchIssues('', ['proj'], 123, 20, 0, '')
1841 expected = work_env.ListResult(['a', 'b'], 20)
1842 self.assertEqual(actual, expected)
1843
1844 @mock.patch('search.frontendsearchpipeline.FrontendSearchPipeline')
1845 def testSearchIssues_NoSuchProject(self, mocked_pipeline):
1846 mocked_instance = mocked_pipeline.return_value
1847 mocked_instance.total_count = 10
1848 mocked_instance.visible_results = ['a', 'b']
1849
1850 with self.assertRaises(exceptions.NoSuchProjectException):
1851 with self.work_env as we:
1852 we.SearchIssues('', ['chicken'], 123, 20, 0, '')
1853
1854 @mock.patch('search.frontendsearchpipeline.FrontendSearchPipeline')
1855 def testListIssues_Normal(self, mocked_pipeline):
1856 """We can do a query that generates some results."""
1857 mocked_instance = mocked_pipeline.return_value
1858 with self.work_env as we:
1859 actual = we.ListIssues('', ['a'], 123, 20, 0, 1, '', '', True)
1860 self.assertEqual(actual, mocked_instance)
1861 mocked_instance.SearchForIIDs.assert_called_once()
1862 mocked_instance.MergeAndSortIssues.assert_called_once()
1863 mocked_instance.Paginate.assert_called_once()
1864
1865 def testListIssues_Error(self):
1866 """Errors are safely reported."""
1867 pass # TODO(jrobbins): add unit test
1868
1869 def testFindIssuePositionInSearch_Normal(self):
1870 """We can find an issue position for the flipper."""
1871 pass # TODO(jrobbins): add unit test
1872
1873 def testFindIssuePositionInSearch_Error(self):
1874 """Errors are safely reported."""
1875 pass # TODO(jrobbins): add unit test
1876
1877 def testGetIssuesDict_Normal(self):
1878 """We can get an existing issue by issue_id."""
1879 issue_1 = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1880 self.services.issue.TestAddIssue(issue_1)
1881 issue_2 = fake.MakeTestIssue(789, 2, 'sum', 'New', 111, issue_id=78902)
1882 self.services.issue.TestAddIssue(issue_2)
1883
1884 with self.work_env as we:
1885 actual = we.GetIssuesDict([78901, 78902])
1886
1887 self.assertEqual({78901: issue_1, 78902: issue_2}, actual)
1888
1889 def testGetIssuesDict_NoPermission(self):
1890 """We reject attempts to get issues the user cannot view."""
1891 issue_1 = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1892 issue_1.labels = ['Restrict-View-CoreTeam']
1893 issue_1.project_name = 'farm-proj'
1894 self.services.issue.TestAddIssue(issue_1)
1895 issue_2 = fake.MakeTestIssue(789, 2, 'sum', 'New', 111, issue_id=78902)
1896 self.services.issue.TestAddIssue(issue_2)
1897 issue_3 = fake.MakeTestIssue(789, 3, 'sum', 'New', 111, issue_id=78903)
1898 issue_3.labels = ['Restrict-View-CoreTeam']
1899 issue_3.project_name = 'farm-proj'
1900 self.services.issue.TestAddIssue(issue_3)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001901 with self.assertRaisesRegex(
Copybara854996b2021-09-07 19:36:02 +00001902 permissions.PermissionException,
1903 'User is not allowed to view issue: farm-proj:1.\n' +
1904 'User is not allowed to view issue: farm-proj:3.'):
1905 with self.work_env as we:
1906 we.GetIssuesDict([78901, 78902, 78903])
1907
1908 def testGetIssuesDict_NoSuchIssue(self):
1909 """We reject attempts to get a non-existent issue."""
1910 issue_1 = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1911 self.services.issue.TestAddIssue(issue_1)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001912 with self.assertRaisesRegex(exceptions.NoSuchIssueException,
1913 'No such issue: 78902\nNo such issue: 78903'):
Copybara854996b2021-09-07 19:36:02 +00001914 with self.work_env as we:
1915 _actual = we.GetIssuesDict([78901, 78902, 78903])
1916
1917 def testGetIssue_Normal(self):
1918 """We can get an existing issue by issue_id."""
1919 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1920 self.services.issue.TestAddIssue(issue)
1921 with self.work_env as we:
1922 actual = we.GetIssue(78901)
1923
1924 self.assertEqual(issue, actual)
1925
1926 def testGetIssue_NoPermission(self):
1927 """We reject attempts to get an issue we don't have permission for."""
1928 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1929 issue.labels = ['Restrict-View-CoreTeam']
1930 self.services.issue.TestAddIssue(issue)
1931
1932 # We should get a permission exception
1933 with self.assertRaises(permissions.PermissionException):
1934 with self.work_env as we:
1935 _actual = we.GetIssue(78901)
1936
1937 # ...unless we have permission to see the issue
1938 self.SignIn(user_id=self.admin_user.user_id)
1939 with self.work_env as we:
1940 actual = we.GetIssue(78901)
1941 self.assertEqual(issue, actual)
1942
1943 def testGetIssue_NoneIssue(self):
1944 """We reject attempts to get a none issue."""
1945 with self.assertRaises(exceptions.InputException):
1946 with self.work_env as we:
1947 _actual = we.GetIssue(None)
1948
1949 def testGetIssue_NoSuchIssue(self):
1950 """We reject attempts to get a non-existent issue."""
1951 with self.assertRaises(exceptions.NoSuchIssueException):
1952 with self.work_env as we:
1953 _actual = we.GetIssue(78901)
1954
1955 def testListReferencedIssues(self):
1956 """We return only existing or visible issues even w/out project names."""
1957 ref_tuples = [
1958 (None, 1), ('other-proj', 1), ('proj', 99),
1959 ('ghost-proj', 1), ('proj', 42), ('other-proj', 1)]
1960 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1961 self.services.issue.TestAddIssue(issue)
1962 private = fake.MakeTestIssue(789, 42, 'sum', 'New', 422, issue_id=78942)
1963 private.labels.append('Restrict-View-CoreTeam')
1964 self.services.issue.TestAddIssue(private)
1965 self.services.project.TestAddProject(
1966 'other-proj', project_id=788)
1967 other_issue = fake.MakeTestIssue(
1968 788, 1, 'sum', 'Fixed', 111, issue_id=78801)
1969 self.services.issue.TestAddIssue(other_issue)
1970
1971 with self.work_env as we:
1972 actual_open, actual_closed = we.ListReferencedIssues(ref_tuples, 'proj')
1973
1974 self.assertEqual([issue], actual_open)
1975 self.assertEqual([other_issue], actual_closed)
1976
1977 def testListReferencedIssues_PreservesOrder(self):
1978 ref_tuples = [('proj', i) for i in range(1, 10)]
1979 # Duplicate some ref_tuples. The result should have no duplicated issues,
1980 # with only the first occurrence being preserved.
1981 ref_tuples += [('proj', 1), ('proj', 5)]
1982 expected_open = [
1983 fake.MakeTestIssue(789, i, 'sum', 'New', 111) for i in range(1, 5)]
1984 expected_closed = [
1985 fake.MakeTestIssue(789, i, 'sum', 'Fixed', 111) for i in range(5, 10)]
1986 for issue in expected_open + expected_closed:
1987 self.services.issue.TestAddIssue(issue)
1988
1989 with self.work_env as we:
1990 actual_open, actual_closed = we.ListReferencedIssues(ref_tuples, 'proj')
1991
1992 self.assertEqual(expected_open, actual_open)
1993 self.assertEqual(expected_closed, actual_closed)
1994
1995 def testGetIssueByLocalID_Normal(self):
1996 """We can get an existing issue by project_id and local_id."""
1997 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
1998 self.services.issue.TestAddIssue(issue)
1999 with self.work_env as we:
2000 actual = we.GetIssueByLocalID(789, 1)
2001
2002 self.assertEqual(issue, actual)
2003
2004 def testGetIssueByLocalID_ProjectNotSpecified(self):
2005 """We reject calls with missing information."""
2006 with self.assertRaises(exceptions.InputException):
2007 with self.work_env as we:
2008 _actual = we.GetIssueByLocalID(None, 1)
2009
2010 def testGetIssueByLocalID_IssueNotSpecified(self):
2011 """We reject calls with missing information."""
2012 with self.assertRaises(exceptions.InputException):
2013 with self.work_env as we:
2014 _actual = we.GetIssueByLocalID(789, None)
2015
2016 def testGetIssueByLocalID_NoSuchIssue(self):
2017 """We reject attempts to get a non-existent issue."""
2018 with self.assertRaises(exceptions.NoSuchIssueException):
2019 with self.work_env as we:
2020 _actual = we.GetIssueByLocalID(789, 1)
2021
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01002022 def testExtractMigratedIdFromLabels(self):
2023 with self.work_env as we:
2024 actual = we.ExtractMigratedIdFromLabels(
2025 ['test-label', 'migrated-to-b-123', 'cob-migrated-to-b-456'])
2026 self.assertEqual('123', actual)
2027
2028 def testExtractMigratedIdFromLabels_NoMigrationLabel(self):
2029 with self.work_env as we:
2030 actual = we.ExtractMigratedIdFromLabels(['test-label', 'to-b-123'])
2031 self.assertEqual(None, actual)
2032
2033 @mock.patch("redirect.redirectissue.RedirectIssue.Get")
2034 def testGetIssueMigratedID_FromDatastore(self, mockRedirect):
2035 mockRedirect.return_value = '123'
2036 with self.work_env as we:
2037 actual = we.GetIssueMigratedID('test', '1', ['migrated-to-b-999'])
2038 self.assertEqual('123', actual)
2039
2040 @mock.patch("redirect.redirectissue.RedirectIssue.Get")
2041 def testGetIssueMigratedID_FromLabels(self, mockRedirect):
2042 mockRedirect.return_value = None
2043 with self.work_env as we:
2044 actual = we.GetIssueMigratedID('test', '1', ['migrated-to-b-999'])
2045 self.assertEqual('999', actual)
2046
2047 @mock.patch("redirect.redirectissue.RedirectIssue.Get")
2048 def testGetIssueMigratedID_None(self, mockRedirect):
2049 mockRedirect.return_value = None
2050 with self.work_env as we:
2051 actual = we.GetIssueMigratedID('test', '1', None)
2052 self.assertEqual(None, actual)
2053
Copybara854996b2021-09-07 19:36:02 +00002054 def testGetRelatedIssueRefs_None(self):
2055 """We handle issues that have no related issues."""
2056 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111)
2057 self.services.issue.TestAddIssue(issue)
2058
2059 with self.work_env as we:
2060 actual = we.GetRelatedIssueRefs([issue])
2061
2062 self.assertEqual({}, actual)
2063
2064 def testGetRelatedIssueRefs_Some(self):
2065 """We can get refs for related issues of a given issue."""
2066 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111)
2067 sooner = fake.MakeTestIssue(789, 2, 'sum', 'New', 111, project_name='proj')
2068 later = fake.MakeTestIssue(789, 3, 'sum', 'New', 111, project_name='proj')
2069 better = fake.MakeTestIssue(789, 4, 'sum', 'New', 111, project_name='proj')
2070 issue.blocked_on_iids.append(sooner.issue_id)
2071 issue.blocking_iids.append(later.issue_id)
2072 issue.merged_into = better.issue_id
2073 self.services.issue.TestAddIssue(issue)
2074 self.services.issue.TestAddIssue(sooner)
2075 self.services.issue.TestAddIssue(later)
2076 self.services.issue.TestAddIssue(better)
2077
2078 with self.work_env as we:
2079 actual = we.GetRelatedIssueRefs([issue])
2080
2081 self.assertEqual(
2082 {sooner.issue_id: ('proj', 2),
2083 later.issue_id: ('proj', 3),
2084 better.issue_id: ('proj', 4)},
2085 actual)
2086
2087 def testGetRelatedIssueRefs_MultipleIssues(self):
2088 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111)
2089 blocking = fake.MakeTestIssue(
2090 789, 2, 'sum', 'New', 111, project_name='proj')
2091 issue2 = fake.MakeTestIssue(789, 3, 'sum', 'New', 111, project_name='proj')
2092 blocked_on = fake.MakeTestIssue(
2093 789, 4, 'sum', 'New', 111, project_name='proj')
2094 issue3 = fake.MakeTestIssue(789, 5, 'sum', 'New', 111, project_name='proj')
2095 merged_into = fake.MakeTestIssue(
2096 789, 6, 'sum', 'New', 111, project_name='proj')
2097
2098 issue.blocked_on_iids.append(blocked_on.issue_id)
2099 issue2.blocking_iids.append(blocking.issue_id)
2100 issue3.merged_into = merged_into.issue_id
2101
2102 self.services.issue.TestAddIssue(issue)
2103 self.services.issue.TestAddIssue(issue2)
2104 self.services.issue.TestAddIssue(issue3)
2105 self.services.issue.TestAddIssue(blocked_on)
2106 self.services.issue.TestAddIssue(blocking)
2107 self.services.issue.TestAddIssue(merged_into)
2108
2109 with self.work_env as we:
2110 actual = we.GetRelatedIssueRefs([issue, issue2, issue3])
2111
2112 self.assertEqual(
2113 {blocking.issue_id: ('proj', 2),
2114 blocked_on.issue_id: ('proj', 4),
2115 merged_into.issue_id: ('proj', 6)},
2116 actual)
2117
2118 def testGetIssueRefs(self):
2119 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, project_name='proj1')
2120 issue2 = fake.MakeTestIssue(789, 3, 'sum', 'New', 111, project_name='proj')
2121 issue3 = fake.MakeTestIssue(789, 5, 'sum', 'New', 111, project_name='proj')
2122
2123 self.services.issue.TestAddIssue(issue)
2124 self.services.issue.TestAddIssue(issue2)
2125 self.services.issue.TestAddIssue(issue3)
2126
2127 with self.work_env as we:
2128 actual = we.GetIssueRefs(
2129 [issue.issue_id, issue2.issue_id, issue3.issue_id])
2130
2131 self.assertEqual(
2132 {issue.issue_id: ('proj1', 1),
2133 issue2.issue_id: ('proj', 3),
2134 issue3.issue_id: ('proj', 5)},
2135 actual)
2136
2137 @mock.patch('businesslogic.work_env.WorkEnv.UpdateIssueApproval')
2138 def testBulkUpdateIssueApprovals(self, mockUpdateIssueApproval):
2139 updated_issues = [78901, 78902]
2140 def side_effect(issue_id, *_args, **_kwargs):
2141 if issue_id in [78903]:
2142 raise permissions.PermissionException
2143 if issue_id in [78904, 78905]:
2144 raise exceptions.NoSuchIssueApprovalException
2145 mockUpdateIssueApproval.side_effect = side_effect
2146
2147 self.SignIn()
2148
2149 approval_delta = tracker_pb2.ApprovalDelta()
2150 issue_ids = self.work_env.BulkUpdateIssueApprovals(
2151 [78901, 78902, 78903, 78904, 78905], 24, self.project, approval_delta,
2152 'comment', send_email=True)
2153 self.assertEqual(issue_ids, updated_issues)
2154 updateIssueApprovalCalls = [
2155 mock.call(
2156 78901, 24, approval_delta, 'comment', False, send_email=False),
2157 mock.call(
2158 78902, 24, approval_delta, 'comment', False, send_email=False),
2159 mock.call(
2160 78903, 24, approval_delta, 'comment', False, send_email=False),
2161 mock.call(
2162 78904, 24, approval_delta, 'comment', False, send_email=False),
2163 mock.call(
2164 78905, 24, approval_delta, 'comment', False, send_email=False),
2165 ]
2166 self.assertEqual(
2167 mockUpdateIssueApproval.call_count, len(updateIssueApprovalCalls))
2168 mockUpdateIssueApproval.assert_has_calls(updateIssueApprovalCalls)
2169
2170 def testBulkUpdateIssueApprovals_AnonUser(self):
2171 approval_delta = tracker_pb2.ApprovalDelta()
2172 with self.assertRaises(permissions.PermissionException):
2173 self.work_env.BulkUpdateIssueApprovals(
2174 [], 24, self.project, approval_delta,
2175 'comment', send_email=True)
2176
2177 def testBulkUpdateIssueApprovals_UserLacksViewPerms(self):
2178 approval_delta = tracker_pb2.ApprovalDelta()
2179 self.SignIn(222)
2180 self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
2181 with self.assertRaises(permissions.PermissionException):
2182 self.work_env.BulkUpdateIssueApprovals(
2183 [], 24, self.project, approval_delta,
2184 'comment', send_email=True)
2185
2186 @mock.patch('businesslogic.work_env.WorkEnv.UpdateIssueApproval')
2187 def testBulkUpdateIssueApprovalsV3(self, mockUpdateIssueApproval):
2188
2189 def side_effect(issue_id, approval_id, *_args, **_kwargs):
2190 return (
2191 tracker_pb2.ApprovalValue(approval_id=approval_id),
2192 tracker_pb2.IssueComment(issue_id=issue_id),
2193 tracker_pb2.Issue(issue_id=issue_id))
2194
2195 mockUpdateIssueApproval.side_effect = side_effect
2196
2197 self.SignIn()
2198
2199 approval_delta = tracker_pb2.ApprovalDelta()
2200 approval_delta_2 = tracker_pb2.ApprovalDelta(approver_ids_add=[111])
2201 deltas_by_issue = [
2202 (78901, 1, approval_delta),
2203 (78901, 1, approval_delta),
2204 (78901, 2, approval_delta),
2205 (78901, 2, approval_delta_2),
2206 (78902, 24, approval_delta),
2207 ]
2208 updated_approval_values = self.work_env.BulkUpdateIssueApprovalsV3(
2209 deltas_by_issue, 'xyz', send_email=True)
2210 expected = []
2211 for iid, aid, _delta in deltas_by_issue:
2212 issue_approval_value_pair = (
2213 tracker_pb2.Issue(issue_id=iid),
2214 tracker_pb2.ApprovalValue(approval_id=aid))
2215 expected.append(issue_approval_value_pair)
2216
2217 self.assertEqual(updated_approval_values, expected)
2218 updateIssueApprovalCalls = []
2219 for iid, aid, delta in deltas_by_issue:
2220 mock_call = mock.call(
2221 iid, aid, delta, 'xyz', False, send_email=True, update_perms=True)
2222 updateIssueApprovalCalls.append(mock_call)
2223 self.assertEqual(mockUpdateIssueApproval.call_count, len(deltas_by_issue))
2224 mockUpdateIssueApproval.assert_has_calls(updateIssueApprovalCalls)
2225
2226 @mock.patch('businesslogic.work_env.WorkEnv.UpdateIssueApproval')
2227 def testBulkUpdateIssueApprovalsV3_PermError(self, mockUpdateIssueApproval):
2228 mockUpdateIssueApproval.side_effect = mock.Mock(
2229 side_effect=permissions.PermissionException())
2230 approval_delta = tracker_pb2.ApprovalDelta()
2231 deltas_by_issue = [(78901, 1, approval_delta)]
2232 with self.assertRaises(permissions.PermissionException):
2233 self.work_env.BulkUpdateIssueApprovalsV3(
2234 deltas_by_issue, 'comment', send_email=True)
2235
2236 @mock.patch('businesslogic.work_env.WorkEnv.UpdateIssueApproval')
2237 def testBulkUpdateIssueApprovalsV3_NotFound(self, mockUpdateIssueApproval):
2238 mockUpdateIssueApproval.side_effect = mock.Mock(
2239 side_effect=exceptions.NoSuchIssueApprovalException())
2240 approval_delta = tracker_pb2.ApprovalDelta()
2241 deltas_by_issue = [(78901, 1, approval_delta)]
2242 with self.assertRaises(exceptions.NoSuchIssueApprovalException):
2243 self.work_env.BulkUpdateIssueApprovalsV3(
2244 deltas_by_issue, 'comment', send_email=True)
2245
2246 def testBulkUpdateIssueApprovalsV3_UserLacksViewPerms(self):
2247 self.SignIn(222)
2248 self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
2249 # No exception raised in v3. Permissions checked in UpdateIssueApprovals.
2250 self.work_env.BulkUpdateIssueApprovalsV3([], 'comment', send_email=True)
2251
2252 @mock.patch(
2253 'features.send_notifications.PrepareAndSendApprovalChangeNotification')
2254 def testUpdateIssueApproval(self, _mockPrepareAndSend):
2255 """We can update an issue's approval_value."""
2256
2257 self.services.issue.DeltaUpdateIssueApproval = mock.Mock()
2258
2259 self.SignIn()
2260
2261 config = fake.MakeTestConfig(789, [], [])
2262 self.services.config.StoreConfig('cnxn', config)
2263
2264 av_24 = tracker_pb2.ApprovalValue(
2265 approval_id=24, approver_ids=[111],
2266 status=tracker_pb2.ApprovalStatus.NOT_SET, set_on=1234, setter_id=999)
2267 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111,
2268 issue_id=78901, approval_values=[av_24])
2269 self.services.issue.TestAddIssue(issue)
2270
2271 delta = tracker_pb2.ApprovalDelta(
2272 status=tracker_pb2.ApprovalStatus.REVIEW_REQUESTED,
2273 set_on=2345,
2274 approver_ids_add=[222],
2275 setter_id=111)
2276
2277 self.work_env.UpdateIssueApproval(78901, 24, delta, 'please review', False)
2278
2279 self.services.issue.DeltaUpdateIssueApproval.assert_called_once_with(
2280 self.mr.cnxn, 111, config, issue, av_24, delta,
2281 comment_content='please review', is_description=False, attachments=None,
2282 kept_attachments=None)
2283
2284 @mock.patch(
2285 'features.send_notifications.PrepareAndSendApprovalChangeNotification')
2286 def testUpdateIssueApproval_IsDescription(self, _mockPrepareAndSend):
2287 """We can update an issue's approval survey."""
2288
2289 self.services.issue.DeltaUpdateIssueApproval = mock.Mock()
2290
2291 self.SignIn()
2292
2293 config = fake.MakeTestConfig(789, [], [])
2294 self.services.config.StoreConfig('cnxn', config)
2295
2296 av_24 = tracker_pb2.ApprovalValue(approval_id=24)
2297 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111,
2298 issue_id=78901, approval_values=[av_24])
2299 self.services.issue.TestAddIssue(issue)
2300
2301 delta = tracker_pb2.ApprovalDelta(setter_id=111)
2302 self.work_env.UpdateIssueApproval(78901, 24, delta, 'better response', True)
2303
2304 self.services.issue.DeltaUpdateIssueApproval.assert_called_once_with(
2305 self.mr.cnxn, 111, config, issue, av_24, delta,
2306 comment_content='better response', is_description=True,
2307 attachments=None, kept_attachments=None)
2308
2309 @mock.patch(
2310 'features.send_notifications.PrepareAndSendApprovalChangeNotification')
2311 def testUpdateIssueApproval_Attachments(self, _mockPrepareAndSend):
2312 """We can attach files as we many an approval change."""
2313 self.services.issue.DeltaUpdateIssueApproval = mock.Mock()
2314
2315 self.SignIn()
2316
2317 config = fake.MakeTestConfig(789, [], [])
2318 self.services.config.StoreConfig('cnxn', config)
2319
2320 av_24 = tracker_pb2.ApprovalValue(
2321 approval_id=24, approver_ids=[111],
2322 status=tracker_pb2.ApprovalStatus.NOT_SET, set_on=1234, setter_id=999)
2323 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111,
2324 issue_id=78901, approval_values=[av_24])
2325 self.services.issue.TestAddIssue(issue)
2326
2327 delta = tracker_pb2.ApprovalDelta(
2328 status=tracker_pb2.ApprovalStatus.REVIEW_REQUESTED,
2329 set_on=2345,
2330 approver_ids_add=[222],
2331 setter_id=111)
2332 attachments = []
2333 self.work_env.UpdateIssueApproval(78901, 24, delta, 'please review', False,
2334 attachments=attachments)
2335
2336 self.services.issue.DeltaUpdateIssueApproval.assert_called_once_with(
2337 self.mr.cnxn, 111, config, issue, av_24, delta,
2338 comment_content='please review', is_description=False,
2339 attachments=attachments, kept_attachments=None)
2340
2341 @mock.patch(
2342 'features.send_notifications.PrepareAndSendApprovalChangeNotification')
2343 @mock.patch(
2344 'tracker.tracker_helpers.FilterKeptAttachments')
2345 def testUpdateIssueApproval_KeptAttachments(
2346 self, mockFilterKeptAttachments, _mockPrepareAndSend):
2347 """We can keep attachments from previous descriptions."""
2348 self.services.issue.DeltaUpdateIssueApproval = mock.Mock()
2349 mockFilterKeptAttachments.return_value = [1, 2]
2350
2351 self.SignIn()
2352
2353 config = fake.MakeTestConfig(789, [], [])
2354 self.services.config.StoreConfig('cnxn', config)
2355
2356 av_24 = tracker_pb2.ApprovalValue(
2357 approval_id=24, approver_ids=[111],
2358 status=tracker_pb2.ApprovalStatus.NOT_SET, set_on=1234, setter_id=999)
2359 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111,
2360 issue_id=78901, approval_values=[av_24])
2361 self.services.issue.TestAddIssue(issue)
2362
2363 delta = tracker_pb2.ApprovalDelta(setter_id=111)
2364 with self.work_env as we:
2365 we.UpdateIssueApproval(
2366 78901, 24, delta, 'Another Desc', True, kept_attachments=[1, 2, 3])
2367
2368 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
2369 mockFilterKeptAttachments.assert_called_once_with(
2370 True, [1, 2, 3], comments, 24)
2371 self.services.issue.DeltaUpdateIssueApproval.assert_called_once_with(
2372 self.mr.cnxn, 111, config, issue, av_24, delta,
2373 comment_content='Another Desc', is_description=True,
2374 attachments=None, kept_attachments=[1, 2])
2375
2376 def testUpdateIssueApproval_TooLongComment(self):
2377 """We raise an exception if too long a comment is used when updating an
2378 issue's approval value."""
2379 self.services.issue.DeltaUpdateIssueApproval = mock.Mock()
2380
2381 self.SignIn()
2382
2383 config = fake.MakeTestConfig(789, [], [])
2384 self.services.config.StoreConfig('cnxn', config)
2385
2386 av_24 = tracker_pb2.ApprovalValue(
2387 approval_id=24,
2388 approver_ids=[111],
2389 status=tracker_pb2.ApprovalStatus.NOT_SET,
2390 set_on=1234,
2391 setter_id=999)
2392 issue = fake.MakeTestIssue(
2393 789,
2394 1,
2395 'summary',
2396 'Available',
2397 111,
2398 issue_id=78901,
2399 approval_values=[av_24])
2400 self.services.issue.TestAddIssue(issue)
2401
2402 delta = tracker_pb2.ApprovalDelta(
2403 status=tracker_pb2.ApprovalStatus.REVIEW_REQUESTED,
2404 set_on=2345,
2405 approver_ids_add=[222])
2406
2407 with self.assertRaises(exceptions.InputException):
2408 long_comment = ' ' + 'c' * tracker_constants.MAX_COMMENT_CHARS + ' '
2409 self.work_env.UpdateIssueApproval(78901, 24, delta, long_comment, False)
2410
2411 def testUpdateIssueApproval_NonExistentUsers(self):
2412 """We raise an exception if adding an approver that does not exist."""
2413 self.services.issue.DeltaUpdateIssueApproval = mock.Mock()
2414
2415 self.SignIn()
2416
2417 config = fake.MakeTestConfig(789, [], [])
2418 self.services.config.StoreConfig('cnxn', config)
2419
2420 av_24 = tracker_pb2.ApprovalValue(
2421 approval_id=24,
2422 approver_ids=[111],
2423 status=tracker_pb2.ApprovalStatus.NOT_SET,
2424 set_on=1234,
2425 setter_id=999)
2426 issue = fake.MakeTestIssue(
2427 789,
2428 1,
2429 'summary',
2430 'Available',
2431 111,
2432 issue_id=78901,
2433 approval_values=[av_24])
2434 self.services.issue.TestAddIssue(issue)
2435
2436 delta = tracker_pb2.ApprovalDelta(
2437 status=tracker_pb2.ApprovalStatus.REVIEW_REQUESTED,
2438 set_on=2345,
2439 approver_ids_add=[9876])
2440
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01002441 with self.assertRaisesRegex(exceptions.InputException,
2442 'users/9876: User does not exist.'):
Copybara854996b2021-09-07 19:36:02 +00002443 comment = 'stuff'
2444 self.work_env.UpdateIssueApproval(78901, 24, delta, comment, False)
2445
2446 @mock.patch(
2447 'features.send_notifications.PrepareAndSendIssueChangeNotification')
2448 def testConvertIssueApprovalsTemplate(self, fake_pasicn):
2449 """We can convert an issue's approvals to match template's approvals."""
2450 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111)
2451 issue.approval_values = [
2452 tracker_pb2.ApprovalValue(
2453 approval_id=3,
2454 phase_id=4,
2455 status=tracker_pb2.ApprovalStatus.APPROVED,
2456 approver_ids=[111],
2457 ),
2458 tracker_pb2.ApprovalValue(
2459 approval_id=4,
2460 phase_id=5,
2461 approver_ids=[111]),
2462 tracker_pb2.ApprovalValue(approval_id=6)]
2463 issue.phases = [
2464 tracker_pb2.Phase(name='Expired', phase_id=4),
2465 tracker_pb2.Phase(name='canary', phase_id=3)]
2466 issue.field_values = [
2467 tracker_bizobj.MakeFieldValue(8, None, 'Pink', None, None, None, False),
2468 tracker_bizobj.MakeFieldValue(
2469 9, None, 'Silver', None, None, None, False, phase_id=3),
2470 tracker_bizobj.MakeFieldValue(
2471 19, None, 'Orange', None, None, None, False, phase_id=4),
2472 ]
2473
2474 self.services.issue._UpdateIssuesApprovals = mock.Mock()
2475 self.SignIn()
2476
2477 template = testing_helpers.DefaultTemplates()[0]
2478 template.approval_values = [
2479 tracker_pb2.ApprovalValue(
2480 approval_id=3,
2481 phase_id=6, # Different phase. Nothing else affected.
2482 approver_ids=[222]),
2483 # No phase. Nothing else affected.
2484 tracker_pb2.ApprovalValue(approval_id=4),
2485 # New approval not already found in issue.
2486 tracker_pb2.ApprovalValue(
2487 approval_id=7,
2488 phase_id=5,
2489 approver_ids=[222]),
2490 ] # No approval 6
2491 template.phases = [tracker_pb2.Phase(name='Canary', phase_id=5),
2492 tracker_pb2.Phase(name='Stable-Exp', phase_id=6)]
2493 self.services.template.GetTemplateByName.return_value = template
2494
2495 config = self.services.config.GetProjectConfig(self.cnxn, 789)
2496 config.approval_defs = [
2497 tracker_pb2.ApprovalDef(approval_id=3, survey='Question3'),
2498 tracker_pb2.ApprovalDef(approval_id=4, survey='Question4'),
2499 tracker_pb2.ApprovalDef(approval_id=7, survey='Question7'),
2500 ]
2501 config.field_defs = [
2502 tracker_pb2.FieldDef(
2503 field_id=3, project_id=789, field_name='Cow'),
2504 tracker_pb2.FieldDef(
2505 field_id=4, project_id=789, field_name='Chicken'),
2506 tracker_pb2.FieldDef(
2507 field_id=6, project_id=789, field_name='Llama'),
2508 tracker_pb2.FieldDef(
2509 field_id=7, project_id=789, field_name='Roo'),
2510 tracker_pb2.FieldDef(
2511 field_id=8, project_id=789, field_name='Salmon'),
2512 tracker_pb2.FieldDef(
2513 field_id=9, project_id=789, field_name='Tuna', is_phase_field=True),
2514 tracker_pb2.FieldDef(
2515 field_id=10, project_id=789, field_name='Clown', is_phase_field=True),
2516 ]
2517 self.work_env.ConvertIssueApprovalsTemplate(
2518 config, issue, 'template_name', 'Convert', send_email=False)
2519
2520 expected_avs = [
2521 tracker_pb2.ApprovalValue(
2522 approval_id=3,
2523 phase_id=6,
2524 status=tracker_pb2.ApprovalStatus.APPROVED,
2525 approver_ids=[111],
2526 ),
2527 tracker_pb2.ApprovalValue(
2528 approval_id=4,
2529 approver_ids=[111]),
2530 tracker_pb2.ApprovalValue(
2531 approval_id=7,
2532 phase_id=5,
2533 approver_ids=[222]),
2534 ]
2535 expected_fvs = [
2536 tracker_bizobj.MakeFieldValue(8, None, 'Pink', None, None, None, False),
2537 tracker_bizobj.MakeFieldValue(
2538 9, None, 'Silver', None, None, None, False, phase_id=5),
2539 ]
2540 self.assertEqual(issue.approval_values, expected_avs)
2541 self.assertEqual(issue.field_values, expected_fvs)
2542 self.assertEqual(issue.phases, template.phases)
2543 self.services.template.GetTemplateByName.assert_called_once_with(
2544 self.mr.cnxn, 'template_name', 789)
2545 fake_pasicn.assert_called_with(
2546 issue.issue_id, 'testing-app.appspot.com', 111, send_email=False,
2547 comment_id=mock.ANY)
2548
2549 def testConvertIssueApprovalsTemplate_NoSuchTemplate(self):
2550 self.SignIn()
2551 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111)
2552 self.services.template.GetTemplateByName.return_value = None
2553 config = self.services.config.GetProjectConfig(self.cnxn, 789)
2554 with self.assertRaises(exceptions.NoSuchTemplateException):
2555 self.work_env.ConvertIssueApprovalsTemplate(
2556 config, issue, 'template_name', 'comment')
2557
2558 def testConvertIssueApprovalsTemplate_TooLongComment(self):
2559 self.SignIn()
2560 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111)
2561 config = self.services.config.GetProjectConfig(self.cnxn, 789)
2562 with self.assertRaises(exceptions.InputException):
2563 long_comment = ' ' + 'c' * tracker_constants.MAX_COMMENT_CHARS + ' '
2564 self.work_env.ConvertIssueApprovalsTemplate(
2565 config, issue, 'template_name', long_comment)
2566
2567 def testConvertIssueApprovalsTemplate_MissingEditPermissions(self):
2568 self.SignIn(self.user_2.user_id)
2569 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', self.user_1.user_id)
2570 config = self.services.config.GetProjectConfig(self.cnxn, 789)
2571 with self.assertRaises(permissions.PermissionException):
2572 self.work_env.ConvertIssueApprovalsTemplate(
2573 config, issue, 'template_name', 'comment')
2574
2575 @mock.patch(
2576 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
2577 @mock.patch(
2578 'features.send_notifications.PrepareAndSendIssueChangeNotification')
2579 def testUpdateIssue_Normal(self, fake_pasicn, fake_pasibn):
2580 """We can update an issue."""
2581 self.SignIn()
2582 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 0)
2583 self.services.issue.TestAddIssue(issue)
2584
2585 fd = tracker_pb2.FieldDef(
2586 field_name='CustomField',
2587 field_id=1,
2588 field_type=tracker_pb2.FieldTypes.STR_TYPE)
2589 res_fd = tracker_pb2.FieldDef(
2590 field_name='ResField',
2591 field_id=2,
2592 field_type=tracker_pb2.FieldTypes.STR_TYPE,
2593 is_restricted_field=True,
2594 admin_ids=[111])
2595 res_fd2 = tracker_pb2.FieldDef(
2596 field_name='ResEnumField',
2597 field_id=3,
2598 field_type=tracker_pb2.FieldTypes.ENUM_TYPE,
2599 is_restricted_field=True,
2600 editor_ids=[111])
2601 config = self.services.config.GetProjectConfig(self.cnxn, 789)
2602 config.field_defs = [fd, res_fd, res_fd2]
2603 self.services.config.StoreConfig(None, config)
2604
2605 fv = tracker_pb2.FieldValue(field_id=1, str_value='Chicken')
2606 res_fv = tracker_pb2.FieldValue(field_id=2, str_value='Dog')
2607 delta = tracker_pb2.IssueDelta(
2608 owner_id=111,
2609 summary='New summary',
2610 cc_ids_add=[333],
2611 field_vals_add=[fv, res_fv],
2612 labels_add=['resenumfield-b'])
2613
2614 with self.work_env as we:
2615 we.UpdateIssue(issue, delta, 'Getting started')
2616
2617 self.assertEqual(111, issue.owner_id)
2618 self.assertEqual('New summary', issue.summary)
2619 self.assertEqual([333], issue.cc_ids)
2620 self.assertEqual([fv, res_fv], issue.field_values)
2621 self.assertEqual(['resenumfield-b'], issue.labels)
2622 self.assertEqual([issue.issue_id], self.services.issue.enqueued_issues)
2623 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
2624 comment_pb = comments[-1]
2625 self.assertFalse(comment_pb.is_description)
2626 fake_pasicn.assert_called_with(
2627 issue.issue_id, 'testing-app.appspot.com', 111, send_email=True,
2628 old_owner_id=0, comment_id=comment_pb.id)
2629 fake_pasibn.assert_called_with(
2630 issue.issue_id, 'testing-app.appspot.com', [], 111, send_email=True)
2631
2632 def testUpdateIssue_RejectEditRestrictedField(self):
2633 """We can update an issue."""
2634 self.SignIn()
2635 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 0)
2636 self.services.issue.TestAddIssue(issue)
2637
2638 fd = tracker_pb2.FieldDef(
2639 field_name='CustomField',
2640 field_id=1,
2641 field_type=tracker_pb2.FieldTypes.STR_TYPE)
2642 res_fd = tracker_pb2.FieldDef(
2643 field_name='ResField',
2644 field_id=2,
2645 field_type=tracker_pb2.FieldTypes.STR_TYPE,
2646 is_restricted_field=True)
2647 res_fd2 = tracker_pb2.FieldDef(
2648 field_name='ResEnumField',
2649 field_id=3,
2650 field_type=tracker_pb2.FieldTypes.ENUM_TYPE,
2651 is_restricted_field=True)
2652 config = self.services.config.GetProjectConfig(self.cnxn, 789)
2653 config.field_defs = [fd, res_fd, res_fd2]
2654 self.services.config.StoreConfig(None, config)
2655
2656 fv = tracker_pb2.FieldValue(field_id=1, str_value='Chicken')
2657 res_fv = tracker_pb2.FieldValue(field_id=2, str_value='Dog')
2658 delta_res_field_val = tracker_pb2.IssueDelta(
2659 owner_id=111,
2660 summary='New summary',
2661 cc_ids_add=[333],
2662 field_vals_add=[fv, res_fv])
2663 delta_res_enum = tracker_pb2.IssueDelta(
2664 owner_id=111,
2665 summary='New summary',
2666 cc_ids_add=[333],
2667 field_vals_add=[fv],
2668 labels_add=['resenumfield-b'])
2669
2670 with self.assertRaises(permissions.PermissionException):
2671 with self.work_env as we:
2672 we.UpdateIssue(issue, delta_res_field_val, 'Getting Started')
2673 with self.assertRaises(permissions.PermissionException):
2674 with self.work_env as we:
2675 we.UpdateIssue(issue, delta_res_enum, 'Getting Started')
2676
2677 @mock.patch(
2678 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
2679 @mock.patch(
2680 'features.send_notifications.PrepareAndSendIssueChangeNotification')
2681 def testUpdateIssue_EditDescription(self, fake_pasicn, fake_pasibn):
2682 """We can edit an issue description."""
2683 self.SignIn()
2684 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2685 self.services.issue.TestAddIssue(issue)
2686 delta = tracker_pb2.IssueDelta()
2687
2688 with self.work_env as we:
2689 we.UpdateIssue(issue, delta, 'New description', is_description=True)
2690
2691 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
2692 comment_pb = comments[-1]
2693 self.assertTrue(comment_pb.is_description)
2694 fake_pasicn.assert_called_with(
2695 issue.issue_id, 'testing-app.appspot.com', 111, send_email=True,
2696 old_owner_id=111, comment_id=comment_pb.id)
2697 fake_pasibn.assert_called_with(
2698 issue.issue_id, 'testing-app.appspot.com', [], 111, send_email=True)
2699
2700 @mock.patch(
2701 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
2702 @mock.patch(
2703 'features.send_notifications.PrepareAndSendIssueChangeNotification')
2704 def testUpdateIssue_NotAllowedToEditDescription(
2705 self, fake_pasicn, fake_pasibn):
2706 """We cannot edit an issue description without EditIssue permission."""
2707 self.SignIn(222)
2708 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2709 self.services.issue.TestAddIssue(issue)
2710 delta = tracker_pb2.IssueDelta()
2711
2712 with self.assertRaises(permissions.PermissionException):
2713 with self.work_env as we:
2714 we.UpdateIssue(issue, delta, 'New description', is_description=True)
2715
2716 fake_pasicn.assert_not_called()
2717 fake_pasibn.assert_not_called()
2718
2719 @mock.patch(
2720 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
2721 @mock.patch(
2722 'features.send_notifications.PrepareAndSendIssueChangeNotification')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01002723 def testUpdateIssue_FreezeLabels(self, fake_pasicn, fake_pasibn):
2724 """We cannot add new labels."""
2725 self.SignIn()
2726 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2727 self.services.issue.TestAddIssue(issue)
2728 delta = tracker_pb2.IssueDelta(labels_add=['freeze_new_label'])
2729
2730 with self.assertRaisesRegex(
2731 exceptions.InputException,
2732 ('The creation of new labels is blocked for the Chromium project'
2733 ' in Monorail. To continue with editing your issue, please'
2734 ' remove: freeze_new_label label\\(s\\)')):
2735 comment = 'Fake comment'
2736 with self.work_env as we:
2737 we.UpdateIssue(issue, delta, comment)
2738
2739 fake_pasicn.assert_not_called()
2740 fake_pasibn.assert_not_called()
2741
2742 @mock.patch(
2743 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
2744 @mock.patch(
2745 'features.send_notifications.PrepareAndSendIssueChangeNotification')
Copybara854996b2021-09-07 19:36:02 +00002746 def testUpdateIssue_EditTooLongComment(self, fake_pasicn, fake_pasibn):
2747 """We cannot edit an issue description with too long a comment."""
2748 self.SignIn(222)
2749 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2750 self.services.issue.TestAddIssue(issue)
2751 delta = tracker_pb2.IssueDelta()
2752
2753 with self.assertRaises(exceptions.InputException):
2754 long_comment = ' ' + 'c' * tracker_constants.MAX_COMMENT_CHARS + ' '
2755 with self.work_env as we:
2756 we.UpdateIssue(issue, delta, long_comment)
2757
2758 fake_pasicn.assert_not_called()
2759 fake_pasibn.assert_not_called()
2760
2761 @mock.patch(
2762 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
2763 @mock.patch(
2764 'features.send_notifications.PrepareAndSendIssueChangeNotification')
2765 def testUpdateIssue_AddTooLongComment(self, fake_pasicn, fake_pasibn):
2766 """We cannot add too long a comment."""
2767 self.SignIn()
2768 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2769 self.services.issue.TestAddIssue(issue)
2770 delta = tracker_pb2.IssueDelta()
2771
2772 with self.assertRaises(exceptions.InputException):
2773 long_comment = ' ' + 'c' * tracker_constants.MAX_COMMENT_CHARS + ' '
2774 with self.work_env as we:
2775 we.UpdateIssue(issue, delta, long_comment)
2776
2777 fake_pasicn.assert_not_called()
2778 fake_pasibn.assert_not_called()
2779
2780 @mock.patch(
2781 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
2782 @mock.patch(
2783 'features.send_notifications.PrepareAndSendIssueChangeNotification')
2784 def testUpdateIssue_AddComment(self, fake_pasicn, fake_pasibn):
2785 """We can add a comment."""
2786 self.SignIn(222)
2787 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2788 self.services.issue.TestAddIssue(issue)
2789 delta = tracker_pb2.IssueDelta()
2790
2791 with self.work_env as we:
2792 we.UpdateIssue(issue, delta, 'New description')
2793
2794 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
2795 comment_pb = comments[-1]
2796 self.assertFalse(comment_pb.is_description)
2797 fake_pasicn.assert_called_with(
2798 issue.issue_id, 'testing-app.appspot.com', 222, send_email=True,
2799 old_owner_id=111, comment_id=comment_pb.id)
2800 fake_pasibn.assert_called_with(
2801 issue.issue_id, 'testing-app.appspot.com', [], 222, send_email=True)
2802
2803 @mock.patch(
2804 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
2805 @mock.patch(
2806 'features.send_notifications.PrepareAndSendIssueChangeNotification')
2807 def testUpdateIssue_AddComment_NoEmail(self, fake_pasicn, fake_pasibn):
2808 """We can add a comment without sending email."""
2809 self.SignIn(222)
2810 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2811 self.services.issue.TestAddIssue(issue)
2812 delta = tracker_pb2.IssueDelta()
2813
2814 with self.work_env as we:
2815 we.UpdateIssue(issue, delta, 'New description', send_email=False)
2816
2817 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
2818 comment_pb = comments[-1]
2819 self.assertFalse(comment_pb.is_description)
2820 fake_pasicn.assert_called_with(
2821 issue.issue_id, 'testing-app.appspot.com', 222, send_email=False,
2822 old_owner_id=111, comment_id=comment_pb.id)
2823 fake_pasibn.assert_called_with(
2824 issue.issue_id, 'testing-app.appspot.com', [], 222, send_email=False)
2825
2826 @mock.patch(
2827 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
2828 @mock.patch(
2829 'features.send_notifications.PrepareAndSendIssueChangeNotification')
2830 @mock.patch('framework.permissions.GetExtraPerms')
2831 def testUpdateIssue_EditOwner(
2832 self, fake_extra_perms, fake_pasicn, fake_pasibn):
2833 """We can edit the owner with the EditIssueOwner permission."""
2834 self.SignIn(222)
2835 fake_extra_perms.return_value = [permissions.EDIT_ISSUE_OWNER]
2836 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2837 self.services.issue.TestAddIssue(issue)
2838 delta = tracker_pb2.IssueDelta(owner_id=0)
2839
2840 with self.work_env as we:
2841 we.UpdateIssue(issue, delta, '')
2842
2843 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
2844 comment_pb = comments[-1]
2845 self.assertFalse(comment_pb.is_description)
2846 self.assertEqual(0, issue.owner_id)
2847 fake_pasicn.assert_called_with(
2848 issue.issue_id, 'testing-app.appspot.com', 222, send_email=True,
2849 old_owner_id=111, comment_id=comment_pb.id)
2850 fake_pasibn.assert_called_with(
2851 issue.issue_id, 'testing-app.appspot.com', [], 222, send_email=True)
2852
2853 @mock.patch(
2854 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
2855 @mock.patch(
2856 'features.send_notifications.PrepareAndSendIssueChangeNotification')
2857 @mock.patch('framework.permissions.GetExtraPerms')
2858 def testUpdateIssue_EditSummary(
2859 self, fake_extra_perms, fake_pasicn, fake_pasibn):
2860 """We can edit the owner with the EditIssueOwner permission."""
2861 self.SignIn(222)
2862 fake_extra_perms.return_value = [permissions.EDIT_ISSUE_SUMMARY]
2863 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2864 self.services.issue.TestAddIssue(issue)
2865 delta = tracker_pb2.IssueDelta(summary='New Summary')
2866
2867 with self.work_env as we:
2868 we.UpdateIssue(issue, delta, '')
2869
2870 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
2871 comment_pb = comments[-1]
2872 self.assertFalse(comment_pb.is_description)
2873 self.assertEqual('New Summary', issue.summary)
2874 fake_pasicn.assert_called_with(
2875 issue.issue_id, 'testing-app.appspot.com', 222, send_email=True,
2876 old_owner_id=111, comment_id=comment_pb.id)
2877 fake_pasibn.assert_called_with(
2878 issue.issue_id, 'testing-app.appspot.com', [], 222, send_email=True)
2879
2880 @mock.patch(
2881 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
2882 @mock.patch(
2883 'features.send_notifications.PrepareAndSendIssueChangeNotification')
2884 @mock.patch('framework.permissions.GetExtraPerms')
2885 def testUpdateIssue_EditStatus(
2886 self, fake_extra_perms, fake_pasicn, fake_pasibn):
2887 """We can edit the owner with the EditIssueOwner permission."""
2888 self.SignIn(222)
2889 fake_extra_perms.return_value = [permissions.EDIT_ISSUE_STATUS]
2890 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2891 self.services.issue.TestAddIssue(issue)
2892 delta = tracker_pb2.IssueDelta(status='Fixed')
2893
2894 with self.work_env as we:
2895 we.UpdateIssue(issue, delta, '')
2896
2897 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
2898 comment_pb = comments[-1]
2899 self.assertFalse(comment_pb.is_description)
2900 self.assertEqual('Fixed', issue.status)
2901 fake_pasicn.assert_called_with(
2902 issue.issue_id, 'testing-app.appspot.com', 222, send_email=True,
2903 old_owner_id=111, comment_id=comment_pb.id)
2904 fake_pasibn.assert_called_with(
2905 issue.issue_id, 'testing-app.appspot.com', [], 222, send_email=True)
2906
2907 @mock.patch(
2908 'features.send_notifications.PrepareAndSendIssueChangeNotification')
2909 @mock.patch('framework.permissions.GetExtraPerms')
2910 def testUpdateIssue_EditCC(self, fake_extra_perms, _fake_pasicn):
2911 """We can edit the owner with the EditIssueOwner permission."""
2912 self.SignIn(222)
2913 fake_extra_perms.return_value = [permissions.EDIT_ISSUE_CC]
2914 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2915 issue.cc_ids = [111]
2916 self.services.issue.TestAddIssue(issue)
2917 delta = tracker_pb2.IssueDelta(cc_ids_add=[222])
2918
2919 with self.work_env as we:
2920 we.UpdateIssue(issue, delta, '')
2921
2922 self.assertEqual([111, 222], issue.cc_ids)
2923 delta = tracker_pb2.IssueDelta(cc_ids_remove=[111])
2924
2925 with self.work_env as we:
2926 we.UpdateIssue(issue, delta, '')
2927
2928 self.assertEqual([222], issue.cc_ids)
2929
2930 def testUpdateIssue_BadOwner(self):
2931 """We reject new issue owners that don't pass validation."""
2932 self.SignIn()
2933 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2934 self.services.issue.TestAddIssue(issue)
2935
2936 # No such user ID.
2937 delta = tracker_pb2.IssueDelta(owner_id=555)
2938 with self.work_env as we:
2939 with self.assertRaises(exceptions.InputException) as cm:
2940 we.UpdateIssue(issue, delta, '')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01002941 self.assertEqual('Issue owner user ID not found.', str(cm.exception))
Copybara854996b2021-09-07 19:36:02 +00002942
2943 # Not a member
2944 delta = tracker_pb2.IssueDelta(owner_id=222)
2945 with self.work_env as we:
2946 with self.assertRaises(exceptions.InputException) as cm:
2947 we.UpdateIssue(issue, delta, '')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01002948 self.assertEqual('Issue owner must be a project member.', str(cm.exception))
Copybara854996b2021-09-07 19:36:02 +00002949
2950 @mock.patch(
2951 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
2952 @mock.patch(
2953 'features.send_notifications.PrepareAndSendIssueChangeNotification')
2954 def testUpdateIssue_MergeInto(self, fake_pasicn, fake_pasibn):
2955 """We can merge Issue 1 (merged_issue) into Issue 2 (merged_into_issue),
2956 including CCs and starrers."""
2957 self.SignIn()
2958 merged_issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
2959 merged_into_issue = fake.MakeTestIssue(789, 2, 'summary2', 'Available', 111)
2960 self.services.issue.TestAddIssue(merged_issue)
2961 self.services.issue.TestAddIssue(merged_into_issue)
2962 delta = tracker_pb2.IssueDelta(
2963 merged_into=merged_into_issue.issue_id, status='Duplicate')
2964
2965 merged_issue.cc_ids = [111, 222, 333, 444]
2966 self.services.issue_star.SetStarsBatch(
2967 'cnxn', 'service', 'config', merged_issue.issue_id, [111, 222, 333],
2968 True)
2969 self.services.issue_star.SetStarsBatch(
2970 'cnxn', 'service', 'config', merged_into_issue.issue_id, [555], True)
2971 with self.work_env as we:
2972 we.UpdateIssue(merged_issue, delta, '')
2973
2974 merged_into_issue_comments = self.services.issue.GetCommentsForIssue(
2975 'cnxn', merged_into_issue.issue_id)
2976
2977 # Original issue marked as duplicate.
2978 self.assertEqual('Duplicate', merged_issue.status)
2979 # Target issue has original issue's CCs.
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01002980 self.assertEqual([111, 222, 333, 444], merged_into_issue.cc_ids)
Copybara854996b2021-09-07 19:36:02 +00002981 # A comment was added to the target issue.
2982 merged_into_issue_comment = merged_into_issue_comments[-1]
2983 self.assertEqual(
2984 'Issue 1 has been merged into this issue.',
2985 merged_into_issue_comment.content)
2986 source_starrers = self.services.issue_star.LookupItemStarrers(
2987 'cnxn', merged_issue.issue_id)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01002988 six.assertCountEqual(self, [111, 222, 333], source_starrers)
Copybara854996b2021-09-07 19:36:02 +00002989 target_starrers = self.services.issue_star.LookupItemStarrers(
2990 'cnxn', merged_into_issue.issue_id)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01002991 six.assertCountEqual(self, [111, 222, 333, 555], target_starrers)
Copybara854996b2021-09-07 19:36:02 +00002992 # Notifications should be sent for both
2993 # the merged issue and the merged_into issue.
2994 merged_issue_comments = self.services.issue.GetCommentsForIssue(
2995 'cnxn', merged_issue.issue_id)
2996 merged_issue_comment = merged_issue_comments[-1]
2997 hostport = 'testing-app.appspot.com'
2998 execute_calls = [
2999 mock.call(
3000 merged_into_issue.issue_id,
3001 hostport,
3002 111,
3003 send_email=True,
3004 comment_id=merged_into_issue_comment.id),
3005 mock.call(
3006 merged_issue.issue_id,
3007 hostport,
3008 111,
3009 send_email=True,
3010 old_owner_id=111,
3011 comment_id=merged_issue_comment.id)
3012 ]
3013 fake_pasicn.assert_has_calls(execute_calls)
3014 self.assertEqual(2, fake_pasicn.call_count)
3015 fake_pasibn.assert_called_once_with(
3016 merged_issue.issue_id, hostport, [], 111, send_email=True)
3017
3018 def testUpdateIssue_MergeIntoRestrictedIssue(self):
3019 """We cannot merge into an issue we cannot view and edit."""
3020 self.SignIn(333)
3021 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
3022 issue2 = fake.MakeTestIssue(789, 2, 'summary2', 'Available', 111)
3023 self.services.issue.TestAddIssue(issue)
3024 self.services.issue.TestAddIssue(issue2)
3025
3026 delta = tracker_pb2.IssueDelta(
3027 merged_into=issue2.issue_id,
3028 status='Duplicate')
3029
3030 issue2.labels = ['Restrict-View-Foo']
3031 with self.work_env as we:
3032 with self.assertRaises(permissions.PermissionException):
3033 we.UpdateIssue(issue, delta, '')
3034
3035 issue2.labels = ['Restrict-EditIssue-Foo']
3036 with self.work_env as we:
3037 with self.assertRaises(permissions.PermissionException):
3038 we.UpdateIssue(issue, delta, '')
3039
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01003040 # Archived project only editable by Owner 111.
3041 self.services.project.TestAddProject(
3042 'proj',
3043 project_id=779,
3044 owner_ids=[111],
3045 state=project_pb2.ProjectState.ARCHIVED)
3046 issue3 = fake.MakeTestIssue(
3047 779, 1, 'issue in archived project', 'Available', 111)
3048 delta = tracker_pb2.IssueDelta(
3049 merged_into=issue3.issue_id, status='Duplicate')
3050 with self.work_env as we:
3051 with self.assertRaises(permissions.PermissionException):
3052 we.UpdateIssue(issue, delta, '')
3053
Copybara854996b2021-09-07 19:36:02 +00003054 # Original issue still available.
3055 self.assertEqual('Available', issue.status)
3056 # Target issue was not modified.
3057 self.assertEqual([], issue2.cc_ids)
3058 # No comment was added.
3059 comments = self.services.issue.GetCommentsForIssue('cnxn', issue2.issue_id)
3060 self.assertEqual(1, len(comments))
3061
3062 def testUpdateIssue_MergeIntoItself(self):
3063 """We cannot merge an issue into itself."""
3064 self.SignIn()
3065 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
3066 self.services.issue.TestAddIssue(issue)
3067 delta = tracker_pb2.IssueDelta(
3068 merged_into=issue.issue_id,
3069 status='Duplicate')
3070
3071 with self.work_env as we:
3072 with self.assertRaises(exceptions.InputException) as cm:
3073 we.UpdateIssue(issue, delta, '')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01003074 self.assertEqual('Cannot merge an issue into itself.', str(cm.exception))
Copybara854996b2021-09-07 19:36:02 +00003075
3076 # Original issue still available.
3077 self.assertEqual('Available', issue.status)
3078 # No comment was added.
3079 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
3080 self.assertEqual(1, len(comments))
3081
3082 @mock.patch(
3083 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
3084 @mock.patch(
3085 'features.send_notifications.PrepareAndSendIssueChangeNotification')
3086 def testUpdateIssue_BlockOn(self, fake_pasicn, fake_pasibn):
3087 """We can block an issue on an existing issue."""
3088 self.SignIn()
3089 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
3090 upstream_issue = fake.MakeTestIssue(789, 2, 'umbrella', 'Available', 111)
3091 self.services.issue.TestAddIssue(issue)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01003092 self.services.issue.TestAddIssue(upstream_issue)
Copybara854996b2021-09-07 19:36:02 +00003093
3094 delta = tracker_pb2.IssueDelta(blocked_on_add=[upstream_issue.issue_id])
3095 with self.work_env as we:
3096 we.UpdateIssue(issue, delta, '')
3097
3098 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
3099 comment_pb = comments[-1]
3100 self.assertEqual([upstream_issue.issue_id], issue.blocked_on_iids)
3101 fake_pasicn.assert_called_with(
3102 issue.issue_id, 'testing-app.appspot.com', 111, send_email=True,
3103 old_owner_id=111, comment_id=comment_pb.id)
3104 fake_pasibn.assert_called_with(
3105 issue.issue_id, 'testing-app.appspot.com', [upstream_issue.issue_id],
3106 111, send_email=True)
3107
3108 @mock.patch(
3109 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
3110 @mock.patch(
3111 'features.send_notifications.PrepareAndSendIssueChangeNotification')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01003112 def testUpdateIssue_BlockOnRestrictedIssue(self, fake_pasicn, fake_pasibn):
3113 """We cannot block an issue on an issue we cannot view and edit."""
3114 self.SignIn(user_id=self.user_3.user_id)
3115 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
3116 issue2 = fake.MakeTestIssue(789, 2, 'summary2', 'Available', 111)
3117 self.services.issue.TestAddIssue(issue)
3118 self.services.issue.TestAddIssue(issue2)
3119
3120 issue2.labels = ['Restrict-View-Foo']
3121 delta = tracker_pb2.IssueDelta(blocked_on_add=[issue2.issue_id])
3122 with self.work_env as we:
3123 with self.assertRaises(permissions.PermissionException):
3124 we.UpdateIssue(issue, delta, '')
3125 issue2.labels = ['Restrict-EditIssue-Foo']
3126 with self.work_env as we:
3127 with self.assertRaises(permissions.PermissionException):
3128 we.UpdateIssue(issue, delta, '')
3129
3130 delta = tracker_pb2.IssueDelta(blocking_add=[issue2.issue_id])
3131 issue2.labels = ['Restrict-View-Bar']
3132 with self.work_env as we:
3133 with self.assertRaises(permissions.PermissionException):
3134 we.UpdateIssue(issue, delta, '')
3135 issue2.labels = ['Restrict-EditIssue-Bar']
3136 with self.work_env as we:
3137 with self.assertRaises(permissions.PermissionException):
3138 we.UpdateIssue(issue, delta, '')
3139
3140 # Archived project only editable by Owner 111.
3141 self.services.project.TestAddProject(
3142 'proj',
3143 project_id=779,
3144 owner_ids=[111],
3145 state=project_pb2.ProjectState.ARCHIVED)
3146 issue3 = fake.MakeTestIssue(
3147 779, 1, 'issue in archived project', 'Available', 111)
3148 delta = tracker_pb2.IssueDelta(blocking_add=[issue3.issue_id])
3149 with self.work_env as we:
3150 with self.assertRaises(permissions.PermissionException):
3151 we.UpdateIssue(issue, delta, '')
3152
3153 # Original issue was not modified.
3154 self.assertEqual(0, len(issue.blocked_on_iids))
3155 self.assertEqual(0, len(issue.blocking_iids))
3156 # No comment was added.
3157 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
3158 self.assertEqual(1, len(comments))
3159 fake_pasicn.assert_not_called()
3160 fake_pasibn.assert_not_called()
3161
3162 @mock.patch(
3163 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
3164 @mock.patch(
3165 'features.send_notifications.PrepareAndSendIssueChangeNotification')
Copybara854996b2021-09-07 19:36:02 +00003166 def testUpdateIssue_BlockOnItself(self, fake_pasicn, fake_pasibn):
3167 """We cannot block an issue on itself."""
3168 self.SignIn()
3169 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
3170 self.services.issue.TestAddIssue(issue)
3171
3172 delta = tracker_pb2.IssueDelta(blocked_on_add=[issue.issue_id])
3173 with self.work_env as we:
3174 with self.assertRaises(exceptions.InputException) as cm:
3175 we.UpdateIssue(issue, delta, '')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01003176 self.assertEqual('Cannot block an issue on itself.', str(cm.exception))
Copybara854996b2021-09-07 19:36:02 +00003177
3178 delta = tracker_pb2.IssueDelta(blocking_add=[issue.issue_id])
3179 with self.work_env as we:
3180 with self.assertRaises(exceptions.InputException) as cm:
3181 we.UpdateIssue(issue, delta, '')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01003182 self.assertEqual('Cannot block an issue on itself.', str(cm.exception))
Copybara854996b2021-09-07 19:36:02 +00003183
3184 # Original issue was not modified.
3185 self.assertEqual(0, len(issue.blocked_on_iids))
3186 self.assertEqual(0, len(issue.blocking_iids))
3187 # No comment was added.
3188 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
3189 self.assertEqual(1, len(comments))
3190 fake_pasicn.assert_not_called()
3191 fake_pasibn.assert_not_called()
3192
3193 @mock.patch(
3194 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
3195 @mock.patch(
3196 'features.send_notifications.PrepareAndSendIssueChangeNotification')
3197 def testUpdateIssue_Attachments(self, fake_pasicn, fake_pasibn):
3198 """We can attach files as we make a change."""
3199 self.SignIn()
3200 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 0)
3201 self.services.issue.TestAddIssue(issue)
3202 delta = tracker_pb2.IssueDelta(
3203 owner_id=111, summary='New summary', cc_ids_add=[333])
3204
3205 attachments = []
3206 with self.work_env as we:
3207 we.UpdateIssue(issue, delta, 'Getting started', attachments=attachments)
3208
3209 self.assertEqual(111, issue.owner_id)
3210 self.assertEqual('New summary', issue.summary)
3211 self.assertEqual([333], issue.cc_ids)
3212 self.assertEqual([issue.issue_id], self.services.issue.enqueued_issues)
3213
3214 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
3215 comment_pb = comments[-1]
3216 self.assertEqual([], comment_pb.attachments)
3217 fake_pasicn.assert_called_with(
3218 issue.issue_id, 'testing-app.appspot.com', 111, send_email=True,
3219 old_owner_id=0, comment_id=comment_pb.id)
3220 fake_pasibn.assert_called_with(
3221 issue.issue_id, 'testing-app.appspot.com', [], 111, send_email=True)
3222
3223 attachments = [
3224 ('README.md', 'readme content', 'text/plain'),
3225 ('hello.txt', 'hello content', 'text/plain')]
3226 with self.work_env as we:
3227 we.UpdateIssue(issue, delta, 'Getting started', attachments=attachments)
3228 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
3229 comment_pb = comments[-1]
3230 self.assertEqual(2, len(comment_pb.attachments))
3231
3232 @mock.patch(
3233 'features.send_notifications.PrepareAndSendIssueChangeNotification')
3234 def testUpdateIssue_KeptAttachments(self, _fake_pasicn):
3235 """We can attach files as we make a change."""
3236 self.SignIn()
3237 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 111)
3238 self.services.issue.TestAddIssue(issue)
3239
3240 # Add some initial attachments
3241 delta = tracker_pb2.IssueDelta()
3242 attachments = [
3243 ('README.md', 'readme content', 'text/plain'),
3244 ('hello.txt', 'hello content', 'text/plain')]
3245 with self.work_env as we:
3246 we.UpdateIssue(
3247 issue, delta, 'New Description', attachments=attachments,
3248 is_description=True)
3249
3250 with self.work_env as we:
3251 we.UpdateIssue(
3252 issue, delta, 'Yet Another Description', is_description=True,
3253 kept_attachments=[1, 2, 3])
3254
3255 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
3256 comment_pb = comments[-1]
3257 self.assertEqual(1, len(comment_pb.attachments))
3258 self.assertEqual('hello.txt', comment_pb.attachments[0].filename)
3259
3260 @mock.patch(
3261 'features.send_notifications.PrepareAndSendIssueBlockingNotification')
3262 @mock.patch(
3263 'features.send_notifications.PrepareAndSendIssueChangeNotification')
3264 def testUpdateIssue_PermissionDenied(self, fake_pasicn, fake_pasibn):
3265 """We reject attempts to update an issue when the user lacks permission."""
3266 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 555)
3267 self.services.issue.TestAddIssue(issue)
3268 delta = tracker_pb2.IssueDelta(
3269 owner_id=222, summary='New summary', cc_ids_add=[333])
3270
3271 with self.work_env as we:
3272 # User is not signed in.
3273 with self.assertRaises(permissions.PermissionException):
3274 we.UpdateIssue(issue, delta, 'I am anon')
3275
3276 # User signed in to acconut that can view but not edit.
3277 self.SignIn(user_id=222)
3278 with self.assertRaises(permissions.PermissionException):
3279 we.UpdateIssue(issue, delta, 'I am not a project member')
3280
3281 # User signed in to acconut that can view and edit, but issue
3282 # restricts edits to a perm that the user lacks.
3283 self.SignIn(user_id=111)
3284 issue.labels.append('Restrict-EditIssue-CoreTeam')
3285 with self.assertRaises(permissions.PermissionException):
3286 we.UpdateIssue(issue, delta, 'I lack CoreTeam')
3287
3288 fake_pasicn.assert_not_called()
3289 fake_pasibn.assert_not_called()
3290
3291 @mock.patch(
3292 'settings.preferred_domains', {'testing-app.appspot.com': 'example.com'})
3293 @mock.patch(
3294 'settings.branded_domains', {'proj': 'branded.com'})
3295 @mock.patch(
3296 'features.send_notifications.PrepareAndSendIssueChangeNotification')
3297 def testUpdateIssue_BrandedDomain(self, fake_pasicn):
3298 """Updating an issue in project with branded domain uses that domain."""
3299 self.SignIn()
3300 issue = fake.MakeTestIssue(789, 1, 'summary', 'Available', 0)
3301 self.services.issue.TestAddIssue(issue)
3302 delta = tracker_pb2.IssueDelta(
3303 owner_id=111, summary='New summary', cc_ids_add=[333])
3304
3305 with self.work_env as we:
3306 we.UpdateIssue(issue, delta, 'Getting started')
3307
3308 comments = self.services.issue.GetCommentsForIssue('cnxn', issue.issue_id)
3309 comment_pb = comments[-1]
3310 hostport = 'branded.com'
3311 fake_pasicn.assert_called_with(
3312 issue.issue_id, hostport, 111, send_email=True,
3313 old_owner_id=0, comment_id=comment_pb.id)
3314
3315 @mock.patch(
3316 'features.send_notifications.PrepareAndSendIssueChangeNotification')
3317 @mock.patch('features.send_notifications.SendIssueBulkChangeNotification')
3318 @mock.patch('time.time')
3319 def testModifyIssues_WeirdDeltas(
3320 self, fake_time, fake_bulk_notify, fake_notify):
3321 """Test that ModifyIssues does not panic with weird deltas."""
3322 fake_time.return_value = self.PAST_TIME
3323
3324 # Issues merge into each other.
3325 issue_merge_a = _Issue(789, 1)
3326 issue_merge_b = _Issue(789, 2)
3327
3328 delta_merge_a = tracker_pb2.IssueDelta(
3329 merged_into=issue_merge_b.issue_id, status='Duplicate')
3330 delta_merge_b = tracker_pb2.IssueDelta(
3331 merged_into=issue_merge_a.issue_id, status='Duplicate')
3332
3333 exp_merge_a = copy.deepcopy(issue_merge_a)
3334 exp_merge_a.merged_into = issue_merge_b.issue_id
3335 exp_merge_a.status = 'Duplicate'
3336 exp_merge_a.status_modified_timestamp = self.PAST_TIME
3337 exp_amendments_merge_a = [
3338 tracker_bizobj.MakeStatusAmendment('Duplicate', ''),
3339 tracker_bizobj.MakeMergedIntoAmendment(
3340 [(issue_merge_b.project_name, issue_merge_b.local_id)], [],
3341 default_project_name=issue_merge_a.project_name)
3342 ]
3343
3344 exp_merge_a_imp_content = work_env.MERGE_COMMENT % issue_merge_b.local_id
3345 exp_merge_b = copy.deepcopy(issue_merge_b)
3346 exp_merge_b.merged_into = exp_merge_a.issue_id
3347 exp_merge_b.status = 'Duplicate'
3348 exp_merge_b.status_modified_timestamp = self.PAST_TIME
3349 exp_amendments_merge_b = [
3350 tracker_bizobj.MakeStatusAmendment('Duplicate', ''),
3351 tracker_bizobj.MakeMergedIntoAmendment(
3352 [(issue_merge_a.project_name, issue_merge_a.local_id)], [],
3353 default_project_name=issue_merge_b.project_name)
3354 ]
3355
3356 exp_merge_b_imp_content = work_env.MERGE_COMMENT % issue_merge_a.local_id
3357
3358 # Issues that block each other.
3359 issue_block_a = _Issue(789, 5)
3360 issue_block_b = _Issue(789, 6)
3361
3362 delta_block_a = tracker_pb2.IssueDelta(
3363 blocking_add=[issue_block_b.issue_id])
3364 delta_block_b = tracker_pb2.IssueDelta(
3365 blocking_add=[issue_block_a.issue_id])
3366
3367 exp_block_a = copy.deepcopy(issue_block_a)
3368 exp_block_a.blocking_iids = [issue_block_b.issue_id]
3369 exp_block_a.blocked_on_iids = [issue_block_b.issue_id]
3370 exp_amendments_block_a = [tracker_bizobj.MakeBlockingAmendment(
3371 [(issue_block_b.project_name, issue_block_b.local_id)], [],
3372 default_project_name=issue_block_a.project_name)]
3373 exp_amendments_block_a_imp = [tracker_bizobj.MakeBlockedOnAmendment(
3374 [(issue_block_b.project_name, issue_block_b.local_id)], [],
3375 default_project_name=issue_block_a.project_name)]
3376
3377 exp_block_b = copy.deepcopy(issue_block_b)
3378 exp_block_b.blocking_iids = [issue_block_a.issue_id]
3379 exp_block_b.blocked_on_iids = [issue_block_a.issue_id]
3380 exp_amendments_block_b = [tracker_bizobj.MakeBlockingAmendment(
3381 [(issue_block_a.project_name, issue_block_a.local_id)], [],
3382 default_project_name=issue_block_b.project_name)]
3383 exp_amendments_block_b_imp = [tracker_bizobj.MakeBlockedOnAmendment(
3384 [(issue_block_a.project_name, issue_block_a.local_id)], [],
3385 default_project_name=issue_block_b.project_name)]
3386
3387 # By default new blocked_on issues that appear in blocked_on_iids
3388 # with no prior rank associated with it are un-ranked and assigned rank 0.
3389 # See SortBlockedOn in issue_svc.py.
3390 exp_block_a.blocked_on_ranks = [0]
3391 exp_block_b.blocked_on_ranks = [0]
3392
3393 self.services.issue.TestAddIssue(issue_merge_a)
3394 self.services.issue.TestAddIssue(issue_merge_b)
3395 self.services.issue.TestAddIssue(issue_block_a)
3396 self.services.issue.TestAddIssue(issue_block_b)
3397
3398 self.mr.cnxn = mock.Mock()
3399 self.mr.cnxn.Commit = mock.Mock()
3400 self.services.issue.EnqueueIssuesForIndexing = mock.Mock()
3401 issue_delta_pairs = [(issue_merge_a.issue_id, delta_merge_a),
3402 (issue_merge_b.issue_id, delta_merge_b),
3403 (issue_block_a.issue_id, delta_block_a),
3404 (issue_block_b.issue_id, delta_block_b)]
3405
3406 content = 'Je suis un ananas.'
3407 self.SignIn(self.user_1.user_id)
3408 send_email = False
3409 with self.work_env as we:
3410 actual_issues = we.ModifyIssues(
3411 issue_delta_pairs,
3412 False,
3413 comment_content=content,
3414 send_email=send_email)
3415
3416 # We expect all issues to have a description comment and the comment(s)
3417 # added from the ModifyIssues() changes.
3418 def CheckComment(
3419 issue_id, exp_amendments, exp_amendments_imp, imp_comment_content=''):
3420 (_desc, comment, comment_imp
3421 ) = self.services.issue.comments_by_iid[issue_id]
3422 self.assertEqual(comment.amendments, exp_amendments)
3423 self.assertEqual(comment.content, content)
3424 self.assertEqual(comment_imp.amendments, exp_amendments_imp)
3425 self.assertEqual(comment_imp.content, imp_comment_content)
3426 return comment, comment_imp
3427
3428 # Merge changes result in a MERGEDINTO Amendment for an
3429 # Issue's mergedInto change (e.g. MergedInto: 1)
3430 # and comment content for the impacted issue's change (with no amendment).
3431 # (e.g. 'Issue 2 has been merged into the this issue.')
3432 comment_merge_a, comment_merge_a_imp = CheckComment(
3433 issue_merge_a.issue_id,
3434 exp_amendments_merge_a, [],
3435 imp_comment_content=exp_merge_a_imp_content)
3436 comment_merge_b, comment_merge_b_imp = CheckComment(
3437 issue_merge_b.issue_id,
3438 exp_amendments_merge_b, [],
3439 imp_comment_content=exp_merge_b_imp_content)
3440
3441 comment_block_a, comment_block_a_imp = CheckComment(
3442 issue_block_a.issue_id, exp_amendments_block_a,
3443 exp_amendments_block_a_imp)
3444 comment_block_b, comment_block_b_imp = CheckComment(
3445 issue_block_b.issue_id, exp_amendments_block_b,
3446 exp_amendments_block_b_imp)
3447
3448 exp_issues = [exp_merge_a, exp_merge_b, exp_block_a, exp_block_b]
3449 self.assertEqual(len(actual_issues), len(exp_issues))
3450 for exp_issue in exp_issues:
3451 # All updated issues should have been fetched from DB, skipping cache.
3452 # So we expect assume_stale=False was applied to all issues during the
3453 # the fetch.
3454 exp_issue.assume_stale = False
3455 # These derived values get set to the following when an issue goes through
3456 # the ApplyFilterRules path. (see filter_helpers._ComputeDerivedFields)
3457 exp_issue.derived_status = ''
3458 exp_issue.derived_owner_id = 0
3459
3460 exp_issue.modified_timestamp = self.PAST_TIME
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01003461 exp_issue.migration_modified_timestamp = self.PAST_TIME
Copybara854996b2021-09-07 19:36:02 +00003462
3463 # Check we successfully updated the issue in our services layer.
3464 self.assertEqual(exp_issue, self.services.issue.GetIssue(
3465 self.cnxn, exp_issue.issue_id))
3466 # Check the issue was successfully returned.
3467 self.assertTrue(exp_issue in actual_issues)
3468
3469 # Check issues enqueued for indexing.
3470 reindex_iids = {issue.issue_id for issue in exp_issues}
3471 self.services.issue.EnqueueIssuesForIndexing.assert_called_once_with(
3472 self.mr.cnxn, reindex_iids, commit=False)
3473 self.mr.cnxn.Commit.assert_called_once()
3474
3475 hostport = 'testing-app.appspot.com'
3476 expected_notify_calls = [
3477 # Notifications for main changes.
3478 mock.call(
3479 issue_merge_a.issue_id,
3480 hostport,
3481 self.user_1.user_id,
3482 old_owner_id=None,
3483 comment_id=comment_merge_a.id,
3484 send_email=send_email),
3485 mock.call(
3486 issue_merge_b.issue_id,
3487 hostport,
3488 self.user_1.user_id,
3489 old_owner_id=None,
3490 comment_id=comment_merge_b.id,
3491 send_email=send_email),
3492 mock.call(
3493 issue_block_a.issue_id,
3494 hostport,
3495 self.user_1.user_id,
3496 old_owner_id=None,
3497 comment_id=comment_block_a.id,
3498 send_email=send_email),
3499 mock.call(
3500 issue_block_b.issue_id,
3501 hostport,
3502 self.user_1.user_id,
3503 old_owner_id=None,
3504 comment_id=comment_block_b.id,
3505 send_email=send_email),
3506 # Notifications for impacted changes.
3507 mock.call(
3508 issue_merge_a.issue_id,
3509 hostport,
3510 self.user_1.user_id,
3511 comment_id=comment_merge_a_imp.id,
3512 send_email=send_email),
3513 mock.call(
3514 issue_merge_b.issue_id,
3515 hostport,
3516 self.user_1.user_id,
3517 comment_id=comment_merge_b_imp.id,
3518 send_email=send_email),
3519 mock.call(
3520 issue_block_a.issue_id,
3521 hostport,
3522 self.user_1.user_id,
3523 comment_id=comment_block_a_imp.id,
3524 send_email=send_email),
3525 mock.call(
3526 issue_block_b.issue_id,
3527 hostport,
3528 self.user_1.user_id,
3529 comment_id=comment_block_b_imp.id,
3530 send_email=send_email),
3531 ]
3532 fake_notify.assert_has_calls(expected_notify_calls, any_order=True)
3533 fake_bulk_notify.assert_not_called()
3534
3535 @mock.patch(
3536 'features.send_notifications.PrepareAndSendIssueChangeNotification')
3537 @mock.patch('features.send_notifications.SendIssueBulkChangeNotification')
3538 @mock.patch('time.time')
3539 def testModifyIssues(self, fake_time, fake_bulk_notify, fake_notify):
3540 fake_time.return_value = self.PAST_TIME
3541
3542 # A main issue with noop delta.
3543 issue_noop = _Issue(789, 1)
3544 issue_noop.labels = ['chicken']
3545 delta_noop = tracker_pb2.IssueDelta(labels_add=issue_noop.labels)
3546
3547 exp_issue_noop = copy.deepcopy(issue_noop)
3548 exp_amendments_noop = []
3549
3550 # A main issue with an empty delta and impacts from
3551 # issue_shared_a and issue_shared_b.
3552 issue_empty = _Issue(789, 2)
3553 delta_empty = tracker_pb2.IssueDelta()
3554
3555 exp_issue_empty = copy.deepcopy(issue_empty)
3556 exp_amendments_empty = []
3557 exp_amendments_empty_imp = []
3558
3559 # A main issue with a shared delta_shared.
3560 issue_shared_a = _Issue(789, 3)
3561 delta_shared = tracker_pb2.IssueDelta(
3562 owner_id=self.user_1.user_id, blocked_on_add=[issue_empty.issue_id])
3563
3564 exp_issue_shared_a = copy.deepcopy(issue_shared_a)
3565 exp_issue_shared_a.owner_modified_timestamp = self.PAST_TIME
3566 exp_issue_shared_a.owner_id = self.user_1.user_id
3567 exp_issue_shared_a.blocked_on_iids.append(issue_empty.issue_id)
3568 # By default new blocked_on issues that appear in blocked_on_iids
3569 # with no prior rank associated with it are un-ranked and assigned rank 0.
3570 # See SortBlockedOn in issue_svc.py.
3571 exp_issue_shared_a.blocked_on_ranks = [0]
3572 exp_amendments_shared_a = [
3573 tracker_bizobj.MakeOwnerAmendment(
3574 delta_shared.owner_id, issue_shared_a.owner_id),
3575 tracker_bizobj.MakeBlockedOnAmendment(
3576 [(issue_empty.project_name, issue_empty.local_id)], [],
3577 default_project_name=issue_shared_a.project_name)]
3578 exp_issue_empty.blocking_iids.append(issue_shared_a.issue_id)
3579
3580 # A main issue with a shared delta_shared.
3581 issue_shared_b = _Issue(789, 4)
3582
3583 exp_issue_shared_b = copy.deepcopy(issue_shared_b)
3584 exp_issue_shared_b.owner_modified_timestamp = self.PAST_TIME
3585 exp_issue_shared_b.owner_id = delta_shared.owner_id
3586 exp_issue_shared_b.blocked_on_iids.append(issue_empty.issue_id)
3587 exp_issue_shared_b.blocked_on_ranks = [0]
3588
3589 exp_amendments_shared_b = [
3590 tracker_bizobj.MakeOwnerAmendment(
3591 delta_shared.owner_id, issue_shared_b.owner_id),
3592 tracker_bizobj.MakeBlockedOnAmendment(
3593 [(issue_empty.project_name, issue_empty.local_id)], [],
3594 default_project_name=issue_shared_b.project_name)]
3595 exp_issue_empty.blocking_iids.append(issue_shared_b.issue_id)
3596
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01003597 added_refs = [(issue_shared_a.project_name, issue_shared_a.local_id),
3598 (issue_shared_b.project_name, issue_shared_b.local_id)]
Copybara854996b2021-09-07 19:36:02 +00003599 exp_amendments_empty_imp.append(tracker_bizobj.MakeBlockingAmendment(
3600 added_refs, [], default_project_name=issue_empty.project_name))
3601
3602 # Issues impacted by issue_unique.
3603 imp_issue_a = _Issue(789, 11)
3604 imp_issue_a.owner_id = self.user_1.user_id
3605 imp_issue_b = _Issue(789, 12)
3606
3607 exp_imp_issue_a = copy.deepcopy(imp_issue_a)
3608 exp_imp_issue_b = copy.deepcopy(imp_issue_b)
3609
3610 # A main issue with a unique delta and impact on imp_issue_{a|b}.
3611 issue_unique = _Issue(789, 5)
3612 issue_unique.merged_into = imp_issue_b.issue_id
3613 delta_unique = tracker_pb2.IssueDelta(
3614 merged_into=imp_issue_a.issue_id, status='Duplicate')
3615
3616 exp_issue_unique = copy.deepcopy(issue_unique)
3617 exp_issue_unique.merged_into = imp_issue_a.issue_id
3618 exp_issue_unique.status = 'Duplicate'
3619 exp_issue_unique.status_modified_timestamp = self.PAST_TIME
3620 exp_amendments_unique = [
3621 tracker_bizobj.MakeStatusAmendment('Duplicate', ''),
3622 tracker_bizobj.MakeMergedIntoAmendment(
3623 [(imp_issue_a.project_name, imp_issue_a.local_id)],
3624 [(imp_issue_b.project_name, imp_issue_b.local_id)],
3625 default_project_name=issue_unique.project_name)
3626 ]
3627
3628 # We star issue_5 and expect this star to be merged into imp_issue.
3629 exp_imp_starrer = 444
3630 self.services.issue_star.SetStar(
3631 self.cnxn, self.services, None, issue_unique.issue_id,
3632 exp_imp_starrer, True)
3633 exp_imp_issue_a.star_count = 1
3634
3635 # Add a FilterRule for star_count to check filter rules are applied.
3636 starred_label = 'starry-night'
3637 self.services.features.TestAddFilterRule(
3638 789, 'stars=1', add_labels=[starred_label])
3639 exp_imp_issue_a.derived_labels.append(starred_label)
3640
3641 # Setting status away from a MERGED type auto-removes any merged_into.
3642 issue_unmerged = _Issue(789, 6)
3643 issue_unmerged.merged_into_external = 'b/123'
3644 issue_unmerged.status = 'Duplicate'
3645 delta_unmerged = tracker_pb2.IssueDelta(status='Available')
3646
3647 exp_issue_unmerged = copy.deepcopy(issue_unmerged)
3648 exp_issue_unmerged.status = 'Available'
3649 exp_issue_unmerged.merged_into_external = ''
3650 exp_issue_unmerged.merged_into = 0
3651 exp_issue_unmerged.status_modified_timestamp = self.PAST_TIME
3652 exp_amendments_unmerged = [
3653 tracker_bizobj.MakeStatusAmendment('Available', 'Duplicate'),
3654 tracker_bizobj.MakeMergedIntoAmendment(
3655 [], [tracker_pb2.DanglingIssueRef(ext_issue_identifier='b/123')])
3656 ]
3657
3658 self.services.issue.TestAddIssue(imp_issue_a)
3659 self.services.issue.TestAddIssue(imp_issue_b)
3660 self.services.issue.TestAddIssue(issue_noop)
3661 self.services.issue.TestAddIssue(issue_empty)
3662 self.services.issue.TestAddIssue(issue_shared_a)
3663 self.services.issue.TestAddIssue(issue_shared_b)
3664 self.services.issue.TestAddIssue(issue_unique)
3665 self.services.issue.TestAddIssue(issue_unmerged)
3666
3667 issue_delta_pairs = [
3668 (issue_noop.issue_id, delta_noop), (issue_empty.issue_id, delta_empty),
3669 (issue_shared_a.issue_id, delta_shared),
3670 (issue_shared_b.issue_id, delta_shared),
3671 (issue_unique.issue_id, delta_unique),
3672 (issue_unmerged.issue_id, delta_unmerged)
3673 ]
3674 self.mr.cnxn = mock.Mock()
3675 self.mr.cnxn.Commit = mock.Mock()
3676 self.services.issue.EnqueueIssuesForIndexing = mock.Mock()
3677 content = 'Je suis un ananas.'
3678 self.SignIn(self.user_1.user_id)
3679 send_email = True
3680 with self.work_env as we:
3681 actual_issues = we.ModifyIssues(
3682 issue_delta_pairs,
3683 False,
3684 comment_content=content,
3685 send_email=send_email)
3686
3687 # Check comments correct.
3688 # We expect all issues to have a description comment and the comment(s)
3689 # added from the ModifyIssues() changes.
3690 (_desc, comment_noop
3691 ) = self.services.issue.comments_by_iid[issue_noop.issue_id]
3692 self.assertEqual(comment_noop.amendments, exp_amendments_noop)
3693 self.assertEqual(comment_noop.content, content)
3694
3695 # Modified issues that are also impacted, get two comments:
3696 # One with the comment content and, direct issue changes defined in a
3697 # paired delta.
3698 # One with the impacted changes with no comment content.
3699 (_desc, comment_empty, comment_empty_imp
3700 ) = self.services.issue.comments_by_iid[issue_empty.issue_id]
3701 self.assertEqual(comment_empty.amendments, exp_amendments_empty)
3702 self.assertEqual(comment_empty.content, content)
3703 self.assertEqual(comment_empty_imp.amendments, exp_amendments_empty_imp)
3704 self.assertEqual(comment_empty_imp.content, '')
3705
3706 [_desc, shared_a_comment] = self.services.issue.comments_by_iid[
3707 issue_shared_a.issue_id]
3708 self.assertEqual(shared_a_comment.amendments, exp_amendments_shared_a)
3709 self.assertEqual(shared_a_comment.content, content)
3710
3711 (_desc, shared_b_comment) = self.services.issue.comments_by_iid[
3712 issue_shared_b.issue_id]
3713 self.assertEqual(shared_b_comment.amendments, exp_amendments_shared_b)
3714 self.assertEqual(shared_b_comment.content, content)
3715
3716 (_desc, unique_comment) = self.services.issue.comments_by_iid[
3717 issue_unique.issue_id]
3718 self.assertEqual(unique_comment.amendments, exp_amendments_unique)
3719 self.assertEqual(unique_comment.content, content)
3720
3721 (_des, unmerged_comment
3722 ) = self.services.issue.comments_by_iid[issue_unmerged.issue_id]
3723 self.assertEqual(unmerged_comment.amendments, exp_amendments_unmerged)
3724 self.assertEqual(unmerged_comment.content, content)
3725
3726 # imp_issue_{a|b} were only an impacted issue and never main issues with
3727 # IssueDelta changes. Only one comment with impacted changes should
3728 # have been added.
3729 (_desc,
3730 imp_a_comment) = self.services.issue.comments_by_iid[imp_issue_a.issue_id]
3731 self.assertEqual(imp_a_comment.amendments, [])
3732 self.assertEqual(
3733 imp_a_comment.content,
3734 'Issue %s has been merged into this issue.\n' % issue_unique.local_id)
3735 (_desc,
3736 imp_b_comment) = self.services.issue.comments_by_iid[imp_issue_b.issue_id]
3737 self.assertEqual(imp_b_comment.amendments, [])
3738 self.assertEqual(
3739 imp_b_comment.content,
3740 'Issue %s has been un-merged from this issue.\n' %
3741 issue_unique.local_id)
3742
3743 # Check stars correct.
3744 self.assertEqual(
3745 [exp_imp_starrer],
3746 self.services.issue_star.stars_by_item_id[imp_issue_a.issue_id])
3747
3748 # Check issues correct.
3749 expected_issues = [
3750 exp_issue_noop, exp_issue_empty, exp_issue_shared_a, exp_issue_shared_b,
3751 exp_issue_unique, exp_imp_issue_a, exp_imp_issue_b, exp_issue_unmerged
3752 ]
3753 # Check we successfully updated these in our services layer.
3754 for exp_issue in expected_issues:
3755 # All updated issues should have been fetched from DB, skipping cache.
3756 # So we expect assume_stale=False was applied to all issues during the
3757 # the fetch.
3758 exp_issue.assume_stale = False
3759 # These derived values get set to the following when an issue goes through
3760 # the ApplyFilterRules path. (see filter_helpers._ComputeDerivedFields)
3761 # issue_noop had no changes so filter rules were never applied to it.
3762 if exp_issue != exp_issue_noop:
3763 exp_issue.derived_status = ''
3764 exp_issue.derived_owner_id = 0
3765
3766 exp_issue.modified_timestamp = self.PAST_TIME
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01003767 exp_issue.migration_modified_timestamp = self.PAST_TIME
Copybara854996b2021-09-07 19:36:02 +00003768
3769 self.assertEqual(
3770 exp_issue, self.services.issue.GetIssue(self.cnxn, exp_issue.issue_id))
3771 # Check the expected issues were successfully returned.
3772 exp_actual_issues = [
3773 exp_issue_noop, exp_issue_empty, exp_issue_shared_a, exp_issue_shared_b,
3774 exp_issue_unique, exp_issue_unmerged
3775 ]
3776 self.assertEqual(len(exp_actual_issues), len(actual_issues))
3777 for issue in actual_issues:
3778 self.assertTrue(issue in exp_actual_issues)
3779
3780 # Check notifications sent.
3781 hostport = 'testing-app.appspot.com'
3782 expected_notify_calls = [
3783 # Notified as a main issue update.
3784 mock.call(
3785 issue_noop.issue_id,
3786 hostport,
3787 self.user_1.user_id,
3788 old_owner_id=None,
3789 comment_id=comment_noop.id,
3790 send_email=send_email),
3791 # Notified as a main issue update.
3792 mock.call(
3793 issue_empty.issue_id,
3794 hostport,
3795 self.user_1.user_id,
3796 old_owner_id=None,
3797 comment_id=comment_empty.id,
3798 send_email=send_email),
3799 # Notified as a main issue update.
3800 mock.call(
3801 issue_unique.issue_id,
3802 hostport,
3803 self.user_1.user_id,
3804 old_owner_id=None,
3805 comment_id=unique_comment.id,
3806 send_email=send_email),
3807 # Notified as a main issue update.
3808 mock.call(
3809 issue_unmerged.issue_id,
3810 hostport,
3811 self.user_1.user_id,
3812 old_owner_id=None,
3813 comment_id=unmerged_comment.id,
3814 send_email=send_email),
3815 # Notified as an impacted issue update.
3816 mock.call(
3817 imp_issue_b.issue_id,
3818 hostport,
3819 self.user_1.user_id,
3820 comment_id=imp_b_comment.id,
3821 send_email=send_email),
3822 # Notified as an impacted issue update.
3823 mock.call(
3824 issue_empty.issue_id,
3825 hostport,
3826 self.user_1.user_id,
3827 comment_id=comment_empty_imp.id,
3828 send_email=send_email),
3829 # Notified as an impacted issue update.
3830 mock.call(
3831 imp_issue_a.issue_id,
3832 hostport,
3833 self.user_1.user_id,
3834 comment_id=imp_a_comment.id,
3835 send_email=send_email)
3836 ]
3837 fake_notify.assert_has_calls(expected_notify_calls)
3838 old_owner_ids = []
3839 shared_amendments = exp_amendments_shared_a + exp_amendments_shared_b
3840 users_by_id = {0: mock.ANY, 111: mock.ANY}
3841 fake_bulk_notify.assert_called_once_with(
3842 {issue_shared_a.issue_id, issue_shared_b.issue_id}, hostport,
3843 old_owner_ids, content, self.user_1.user_id, shared_amendments,
3844 send_email, users_by_id)
3845
3846 # Check issues enqueued for indexing.
3847 reindex_iids = {issue.issue_id for issue in expected_issues}
3848 self.services.issue.EnqueueIssuesForIndexing.assert_called_once_with(
3849 self.mr.cnxn, reindex_iids, commit=False)
3850 self.mr.cnxn.Commit.assert_called_once()
3851
3852 @mock.patch(
3853 'features.send_notifications.PrepareAndSendIssueChangeNotification')
3854 @mock.patch('features.send_notifications.SendIssueBulkChangeNotification')
3855 @mock.patch('time.time')
3856 def testModifyIssues_ComponentModified(
3857 self, fake_time, fake_bulk_notify, fake_notify):
3858 fake_time.return_value = self.PAST_TIME
3859
3860 issue = _Issue(789, 1)
3861 issue.component_ids = [self.component_id_1]
3862 delta = tracker_pb2.IssueDelta(
3863 comp_ids_add=[self.component_id_2],
3864 comp_ids_remove=[self.component_id_1])
3865
3866 exp_issue = copy.deepcopy(issue)
3867
3868 self.services.issue.TestAddIssue(issue)
3869
3870 issue_delta_pairs = [(issue.issue_id, delta)]
3871 self.mr.cnxn = mock.Mock()
3872 self.mr.cnxn.Commit = mock.Mock()
3873 self.services.issue.EnqueueIssuesForIndexing = mock.Mock()
3874 content = 'Modifying component'
3875 self.SignIn(self.user_1.user_id)
3876 send_email = True
3877
3878 with self.work_env as we:
3879 we.ModifyIssues(
3880 issue_delta_pairs,
3881 False,
3882 comment_content=content,
3883 send_email=send_email)
3884
3885 exp_issue.modified_timestamp = self.PAST_TIME
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01003886 exp_issue.migration_modified_timestamp = self.PAST_TIME
Copybara854996b2021-09-07 19:36:02 +00003887 exp_issue.component_modified_timestamp = self.PAST_TIME
3888 exp_issue.component_ids = [self.component_id_2]
3889
3890 exp_issue.derived_status = ''
3891 exp_issue.derived_owner_id = 0
3892 exp_issue.assume_stale = False
3893
3894 self.assertEqual(
3895 exp_issue, self.services.issue.GetIssue(self.cnxn, exp_issue.issue_id))
3896
3897 fake_bulk_notify.assert_not_called()
3898 fake_notify.assert_called()
3899
3900 @mock.patch(
3901 'features.send_notifications.PrepareAndSendIssueChangeNotification')
3902 @mock.patch('features.send_notifications.SendIssueBulkChangeNotification')
3903 @mock.patch('time.time')
3904 def testModifyIssues_StatusModified(
3905 self, fake_time, fake_bulk_notify, fake_notify):
3906 fake_time.return_value = self.PAST_TIME
3907
3908 issue = _Issue(789, 1)
3909 issue.status = 'New'
3910 delta = tracker_pb2.IssueDelta(status='Fixed')
3911
3912 exp_issue = copy.deepcopy(issue)
3913
3914 self.services.issue.TestAddIssue(issue)
3915
3916 issue_delta_pairs = [(issue.issue_id, delta)]
3917 self.mr.cnxn = mock.Mock()
3918 self.mr.cnxn.Commit = mock.Mock()
3919 self.services.issue.EnqueueIssuesForIndexing = mock.Mock()
3920 content = 'Modifying status'
3921 self.SignIn(self.user_1.user_id)
3922 send_email = True
3923
3924 with self.work_env as we:
3925 we.ModifyIssues(
3926 issue_delta_pairs,
3927 False,
3928 comment_content=content,
3929 send_email=send_email)
3930
3931 exp_issue.modified_timestamp = self.PAST_TIME
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01003932 exp_issue.migration_modified_timestamp = self.PAST_TIME
Copybara854996b2021-09-07 19:36:02 +00003933 exp_issue.status_modified_timestamp = self.PAST_TIME
3934 exp_issue.closed_timestamp = self.PAST_TIME
3935 exp_issue.status = 'Fixed'
3936
3937 exp_issue.derived_status = ''
3938 exp_issue.derived_owner_id = 0
3939 exp_issue.assume_stale = False
3940
3941 self.assertEqual(
3942 exp_issue, self.services.issue.GetIssue(self.cnxn, exp_issue.issue_id))
3943
3944 fake_bulk_notify.assert_not_called()
3945 fake_notify.assert_called()
3946
3947 # We must redirect the testing environment's default domain to a
3948 # non-appspot.com one, in order for the per-project branded domains to get
3949 # used. See framework_helpers.GetNeededDomain().
3950 @mock.patch(
3951 'settings.preferred_domains', {'testing-app.appspot.com': 'example.com'})
3952 @mock.patch(
3953 'settings.branded_domains', {
3954 'proj-783': '783.com', 'proj-782': '782.com', 'proj-781': '781.com'})
3955 @mock.patch(
3956 'features.send_notifications.PrepareAndSendIssueChangeNotification')
3957 @mock.patch('features.send_notifications.SendIssueBulkChangeNotification')
3958 @mock.patch('time.time')
3959 def testModifyIssues_MultiProjectChanges(
3960 self, fake_time, fake_bulk_notify, fake_notify):
3961 fake_time.return_value = self.PAST_TIME
3962 self.services.project.TestAddProject(
3963 'proj-783', project_id=783, committer_ids=[self.user_1.user_id])
3964 self.services.project.TestAddProject(
3965 'proj-782', project_id=782, committer_ids=[self.user_1.user_id])
3966 self.services.project.TestAddProject(
3967 'proj-781', project_id=781, committer_ids=[self.user_1.user_id])
3968 delta = tracker_pb2.IssueDelta(cc_ids_add=[self.user_2.user_id])
3969
3970 def setUpIssue(pid, local_id):
3971 issue = _Issue(pid, local_id)
3972 exp_amendments = [tracker_bizobj.MakeCcAmendment(delta.cc_ids_add, [])]
3973 exp_issue = copy.deepcopy(issue)
3974 exp_issue.cc_ids.extend(delta.cc_ids_add)
3975 exp_issue.modified_timestamp = self.PAST_TIME
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01003976 exp_issue.migration_modified_timestamp = self.PAST_TIME
Copybara854996b2021-09-07 19:36:02 +00003977 return issue, exp_amendments, exp_issue
3978
3979 # We expect fake_bulk_notify to send these issues' notifications.
3980 issue_p1a, exp_amendments_p1a, exp_p1a = setUpIssue(781, 1)
3981 issue_p1b, exp_amendments_p1b, exp_p1b = setUpIssue(781, 2)
3982
3983 # We expect fake_notify to send this issue's notification.
3984 issue_p2, exp_amendments_p2, exp_p2 = setUpIssue(782, 1)
3985
3986 # We expect fake_bulk_notify to send these issues' notifications.
3987 issue_p3a, exp_amendments_p3a, exp_p3a = setUpIssue(783, 1)
3988 issue_p3b, exp_amendments_p3b, exp_p3b = setUpIssue(783, 2)
3989
3990 self.services.issue.TestAddIssue(issue_p1a)
3991 self.services.issue.TestAddIssue(issue_p1b)
3992 self.services.issue.TestAddIssue(issue_p2)
3993 self.services.issue.TestAddIssue(issue_p3a)
3994 self.services.issue.TestAddIssue(issue_p3b)
3995
3996 self.mr.cnxn = mock.Mock()
3997 self.mr.cnxn.Commit = mock.Mock()
3998 self.services.issue.EnqueueIssuesForIndexing = mock.Mock()
3999 issue_delta_pairs = [(issue_p1a.issue_id, delta),
4000 (issue_p1b.issue_id, delta),
4001 (issue_p2.issue_id, delta),
4002 (issue_p3a.issue_id, delta),
4003 (issue_p3b.issue_id, delta)]
4004 self.SignIn(self.user_1.user_id)
4005 content = None
4006 send_email = True
4007 with self.work_env as we:
4008 actual_issues = we.ModifyIssues(
4009 issue_delta_pairs, False, send_email=send_email)
4010
4011 # Check comments.
4012 # We expect all issues to have a description comment and the comment(s)
4013 # added from the ModifyIssues() changes.
4014 def CheckComment(issue_id, exp_amendments):
4015 (_desc, comment) = self.services.issue.comments_by_iid[issue_id]
4016 self.assertEqual(comment.amendments, exp_amendments)
4017 self.assertEqual(comment.content, content)
4018 return comment
4019
4020 _comment_p1a = CheckComment(issue_p1a.issue_id, exp_amendments_p1a)
4021 _comment_p1b = CheckComment(issue_p1b.issue_id, exp_amendments_p1b)
4022 comment_p2 = CheckComment(issue_p2.issue_id, exp_amendments_p2)
4023 _comment_p3a = CheckComment(issue_p3a.issue_id, exp_amendments_p3a)
4024 _comment_p3b = CheckComment(issue_p3b.issue_id, exp_amendments_p3b)
4025
4026 # Check issues.
4027 exp_issues = [exp_p1a, exp_p1b, exp_p2, exp_p3a, exp_p3b]
4028 for exp_issue in exp_issues:
4029 # All updated issues should have been fetched from DB, skipping cache.
4030 # So we expect assume_stale=False was applied to all issues during the
4031 # the fetch.
4032 exp_issue.assume_stale = False
4033 # These derived values get set to the following when an issue goes through
4034 # the ApplyFilterRules path. (see filter_helpers._ComputeDerivedFields)
4035 exp_issue.derived_status = ''
4036 exp_issue.derived_owner_id = 0
4037 # Check we successfully updated these issues in our services layer.
4038 self.assertEqual(exp_issue, self.services.issue.GetIssue(
4039 self.cnxn, exp_issue.issue_id))
4040 # Check the expected issues were successfully returned.
4041 self.assertTrue(exp_issue in actual_issues)
4042
4043 # Check issues enqueued for indexing.
4044 reindex_iids = {issue.issue_id for issue in exp_issues}
4045 self.services.issue.EnqueueIssuesForIndexing.assert_called_once_with(
4046 self.mr.cnxn, reindex_iids, commit=False)
4047 self.mr.cnxn.Commit.assert_called_once()
4048
4049 # Check notifications.
4050 p2_hostport = '782.com'
4051 fake_notify.assert_called_once_with(
4052 issue_p2.issue_id,
4053 p2_hostport,
4054 self.user_1.user_id,
4055 old_owner_id=None,
4056 comment_id=comment_p2.id,
4057 send_email=send_email)
4058
4059 p1_hostport = '781.com'
4060 p1_amendments = exp_amendments_p1a + exp_amendments_p1b
4061 p3_hostport = '783.com'
4062 p3_amendments = exp_amendments_p3a + exp_amendments_p3b
4063 users_by_id = {222: mock.ANY}
4064 old_owners = []
4065 expected_bulk_calls = [
4066 mock.call({issue_p3a.issue_id, issue_p3b.issue_id}, p3_hostport,
4067 old_owners, content, self.user_1.user_id, p3_amendments,
4068 send_email, users_by_id),
4069 mock.call({issue_p1a.issue_id, issue_p1b.issue_id}, p1_hostport,
4070 old_owners, content, self.user_1.user_id, p1_amendments,
4071 send_email, users_by_id)]
4072 fake_bulk_notify.assert_has_calls(expected_bulk_calls, any_order=True)
4073
4074 def testModifyIssues_PermDenied(self):
4075 """Test that AssertUsercanModifyIssues is called."""
4076 issue = _Issue(789, 1)
4077 delta = tracker_pb2.IssueDelta(labels_add=['some-label'])
4078 non_member = self.services.user.TestAddUser('non_member@example.com', 666)
4079 self.services.issue.TestAddIssue(issue)
4080 self.SignIn(non_member.user_id)
4081 with self.assertRaises(permissions.PermissionException):
4082 with self.work_env as we:
4083 we.ModifyIssues(
4084 [(issue.issue_id, delta)], False, comment_content='bad chicken')
4085
4086 # Detailed change validation testing happens in tracker_helpers_test.
4087 def testModifyIssues_InvalidChange(self):
4088 """Test that we check issue change validity."""
4089 non_member = self.services.user.TestAddUser('non_member@example.com', 666)
4090 issue = _Issue(789, 1)
4091 delta = tracker_pb2.IssueDelta(owner_id=non_member.user_id)
4092 self.services.issue.TestAddIssue(issue)
4093 self.SignIn(self.user_1.user_id)
4094 with self.assertRaises(exceptions.InputException):
4095 with self.work_env as we:
4096 we.ModifyIssues(
4097 [(issue.issue_id, delta)], False, comment_content='bad chicken')
4098
4099 @mock.patch(
4100 'features.send_notifications.PrepareAndSendIssueChangeNotification')
4101 @mock.patch('features.send_notifications.SendIssueBulkChangeNotification')
4102 def testModifyIssues_Noop(self, fake_bulk_notify, fake_notify):
4103 issue_empty = _Issue(789, 1)
4104 delta_empty = tracker_pb2.IssueDelta()
4105
4106 issue_noop = _Issue(789, 2)
4107 issue_noop.owner_id = self.user_2.user_id
4108 delta_noop = tracker_pb2.IssueDelta(owner_id=issue_noop.owner_id)
4109
4110 delta_noop_shared = tracker_pb2.IssueDelta(owner_id=issue_noop.owner_id)
4111 issue_noop_shared_a = _Issue(789, 3)
4112 issue_noop_shared_a.owner_id = delta_noop_shared.owner_id
4113 issue_noop_shared_b = _Issue(789, 4)
4114 issue_noop_shared_b.owner_id = delta_noop_shared.owner_id
4115
4116 self.services.issue.TestAddIssue(issue_empty)
4117 self.services.issue.TestAddIssue(issue_noop)
4118 self.services.issue.TestAddIssue(issue_noop_shared_a)
4119 self.services.issue.TestAddIssue(issue_noop_shared_b)
4120
4121 exp_issues = [
4122 copy.deepcopy(issue_empty),
4123 copy.deepcopy(issue_noop),
4124 copy.deepcopy(issue_noop_shared_a),
4125 copy.deepcopy(issue_noop_shared_b)
4126 ]
4127
4128 issue_delta_pairs = [(issue_empty.issue_id, delta_empty),
4129 (issue_noop.issue_id, delta_noop),
4130 (issue_noop_shared_a.issue_id, delta_noop_shared),
4131 (issue_noop_shared_b.issue_id, delta_noop_shared)]
4132
4133
4134 self.mr.cnxn = mock.Mock()
4135 self.mr.cnxn.Commit = mock.Mock()
4136 self.services.issue.UpdateIssue = mock.Mock()
4137 self.services.issue_star.SetStarsBatch_SkipIssueUpdate = mock.Mock()
4138 self.services.issue.CreateIssueComment = mock.Mock()
4139 self.services.project.UpdateProject = mock.Mock()
4140 self.services.issue.EnqueueIssuesForIndexing = mock.Mock()
4141 self.SignIn(self.user_1.user_id)
4142 with self.work_env as we:
4143 issues = we.ModifyIssues(issue_delta_pairs, False, send_email=True)
4144
4145 for exp_issue in exp_issues:
4146 exp_issue.assume_stale = False
4147 # Check issues remained the same with no changes.
4148 self.assertEqual(
4149 exp_issue,
4150 self.services.issue.GetIssue(self.cnxn, exp_issue.issue_id))
4151
4152 self.assertFalse(issues)
4153 self.services.issue.UpdateIssue.assert_not_called()
4154 self.services.issue_star.SetStarsBatch_SkipIssueUpdate.assert_not_called()
4155 self.services.issue.CreateIssueComment.assert_not_called()
4156 self.services.project.UpdateProject.assert_not_called()
4157 self.services.issue.EnqueueIssuesForIndexing.assert_not_called()
4158 fake_bulk_notify.assert_not_called()
4159 fake_notify.assert_not_called()
4160 self.mr.cnxn.Commit.assert_not_called()
4161
4162 @mock.patch(
4163 'features.send_notifications.PrepareAndSendIssueChangeNotification')
4164 @mock.patch('features.send_notifications.SendIssueBulkChangeNotification')
4165 @mock.patch('time.time')
4166 def testModifyIssues_CommentWithNoChanges(
4167 self, fake_time, fake_bulk_notify, fake_notify):
4168 fake_time.return_value = self.PAST_TIME
4169
4170 issue = _Issue(789, 1)
4171 delta_empty = tracker_pb2.IssueDelta()
4172
4173 exp_issue = copy.deepcopy(issue)
4174 exp_issue.modified_timestamp = self.PAST_TIME
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01004175 exp_issue.migration_modified_timestamp = self.PAST_TIME
Copybara854996b2021-09-07 19:36:02 +00004176 exp_issue.assume_stale = False
4177
4178 self.services.issue.TestAddIssue(issue)
4179
4180 issue_delta_pairs = [(issue.issue_id, delta_empty)]
4181
4182 self.mr.cnxn = mock.Mock()
4183 self.mr.cnxn.Commit = mock.Mock()
4184 self.services.issue.UpdateIssue = mock.Mock()
4185 self.services.issue_star.SetStarsBatch_SkipIssueUpdate = mock.Mock()
4186 self.services.issue.CreateIssueComment = mock.Mock()
4187 self.services.project.UpdateProject = mock.Mock()
4188 self.services.issue.EnqueueIssuesForIndexing = mock.Mock()
4189 self.SignIn(self.user_1.user_id)
4190
4191 with self.work_env as we:
4192 issues = we.ModifyIssues(
4193 issue_delta_pairs, False, comment_content='invisible chickens')
4194
4195 self.assertEqual(len(issues), 1)
4196 self.assertEqual(exp_issue, issues[0])
4197 self.assertEqual(
4198 exp_issue, self.services.issue.GetIssue(self.cnxn, exp_issue.issue_id))
4199
4200 self.services.issue.UpdateIssue.assert_not_called()
4201 self.services.issue_star.SetStarsBatch_SkipIssueUpdate.assert_not_called()
4202 self.services.issue.CreateIssueComment.assert_called()
4203 self.services.project.UpdateProject.assert_not_called()
4204 self.services.issue.EnqueueIssuesForIndexing.assert_called()
4205
4206 fake_bulk_notify.assert_not_called()
4207 fake_notify.assert_called()
4208 self.mr.cnxn.Commit.assert_called()
4209 # The closed_timestamp has ben reset to its default value of 0.
4210 self.assertEqual(
4211 0,
4212 self.services.issue.GetIssue(self.cnxn,
4213 exp_issue.issue_id).closed_timestamp)
4214
4215 @mock.patch(
4216 'features.send_notifications.PrepareAndSendIssueChangeNotification')
4217 @mock.patch('features.send_notifications.SendIssueBulkChangeNotification')
4218 @mock.patch('time.time')
4219 def testModifyIssues_AttachmentsWithNoChanges(
4220 self, fake_time, fake_bulk_notify, fake_notify):
4221
4222 fake_time.return_value = self.PAST_TIME
4223
4224 issue = _Issue(789, 1)
4225 delta_empty = tracker_pb2.IssueDelta()
4226
4227 exp_issue = copy.deepcopy(issue)
4228 exp_issue.modified_timestamp = self.PAST_TIME
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01004229 exp_issue.migration_modified_timestamp = self.PAST_TIME
Copybara854996b2021-09-07 19:36:02 +00004230 exp_issue.assume_stale = False
4231
4232 self.services.issue.TestAddIssue(issue)
4233
4234 issue_delta_pairs = [(issue.issue_id, delta_empty)]
4235
4236 self.mr.cnxn = mock.Mock()
4237 self.mr.cnxn.Commit = mock.Mock()
4238 self.services.issue.UpdateIssue = mock.Mock()
4239 self.services.issue_star.SetStarsBatch_SkipIssueUpdate = mock.Mock()
4240 self.services.issue.CreateIssueComment = mock.Mock()
4241 self.services.project.UpdateProject = mock.Mock()
4242 self.services.issue.EnqueueIssuesForIndexing = mock.Mock()
4243 self.SignIn(self.user_1.user_id)
4244
4245 upload = framework_helpers.AttachmentUpload(
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01004246 'BEAR-necessities', b'Forget about your worries and your strife',
Copybara854996b2021-09-07 19:36:02 +00004247 'text/plain')
4248
4249 with self.work_env as we:
4250 issues = we.ModifyIssues(issue_delta_pairs, attachment_uploads=[upload])
4251
4252 self.assertEqual(len(issues), 1)
4253 self.assertEqual(exp_issue, issues[0])
4254 self.assertEqual(
4255 exp_issue, self.services.issue.GetIssue(self.cnxn, exp_issue.issue_id))
4256
4257 self.services.issue.UpdateIssue.assert_not_called()
4258 self.services.issue_star.SetStarsBatch_SkipIssueUpdate.assert_not_called()
4259 self.services.issue.CreateIssueComment.assert_called()
4260 self.services.project.UpdateProject.assert_called()
4261 self.services.issue.EnqueueIssuesForIndexing.assert_called()
4262
4263 fake_bulk_notify.assert_not_called()
4264 fake_notify.assert_called()
4265 self.mr.cnxn.Commit.assert_called()
4266
4267 @mock.patch(
4268 'features.send_notifications.PrepareAndSendIssueChangeNotification')
4269 @mock.patch('features.send_notifications.SendIssueBulkChangeNotification')
4270 def testModifyIssues_Empty(self, fake_bulk_notify, fake_notify):
4271 self.mr.cnxn = mock.Mock()
4272 self.mr.cnxn.Commit = mock.Mock()
4273 self.services.issue.UpdateIssue = mock.Mock()
4274 self.services.issue_star.SetStarsBatch_SkipIssueUpdate = mock.Mock()
4275 self.services.issue.CreateIssueComment = mock.Mock()
4276 self.services.issue.EnqueueIssuesForIndexing = mock.Mock()
4277 with self.work_env as we:
4278 issues = we.ModifyIssues([], False, comment_content='invisible chickens')
4279
4280 self.assertFalse(issues)
4281 self.services.issue.UpdateIssue.assert_not_called()
4282 self.services.issue_star.SetStarsBatch_SkipIssueUpdate.assert_not_called()
4283 self.services.issue.CreateIssueComment.assert_not_called()
4284 self.services.issue.EnqueueIssuesForIndexing.assert_not_called()
4285 fake_bulk_notify.assert_not_called()
4286 fake_notify.assert_not_called()
4287 self.mr.cnxn.Commit.assert_not_called()
4288
4289
4290 def testModifyIssuesBulkNotifyForDelta(self):
4291 # Integrate tested in ModifyIssues tests as the main concern is
4292 # if BulkNotify and Notify work correctly together in the ModifyIssues
4293 # context.
4294 pass
4295
4296 def testModifyIssuesNotifyForDelta(self):
4297 # Integrate tested in ModifyIssues tests as the main concern is
4298 # if BulkNotify and Notify work correctly together in the ModifyIssues
4299 # context.
4300 pass
4301
4302 def testDeleteIssue(self):
4303 """We can mark and unmark an issue as deleted."""
4304 self.SignIn(user_id=self.admin_user.user_id)
4305 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
4306 self.services.issue.TestAddIssue(issue)
4307 with self.work_env as we:
4308 _actual = we.DeleteIssue(issue, True)
4309 self.assertTrue(issue.deleted)
4310 with self.work_env as we:
4311 _actual = we.DeleteIssue(issue, False)
4312 self.assertFalse(issue.deleted)
4313
4314 def testFlagIssue_Normal(self):
4315 """Users can mark and unmark an issue as spam."""
4316 self.services.user.TestAddUser('user222@example.com', 222)
4317 self.SignIn(user_id=222)
4318 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
4319 self.services.issue.TestAddIssue(issue)
4320 with self.work_env as we:
4321 we.FlagIssues([issue], True)
4322 self.assertEqual(
4323 [222], self.services.spam.reports_by_issue_id[78901])
4324 self.assertNotIn(
4325 222, self.services.spam.manual_verdicts_by_issue_id[78901])
4326 with self.work_env as we:
4327 we.FlagIssues([issue], False)
4328 self.assertEqual(
4329 [], self.services.spam.reports_by_issue_id[78901])
4330 self.assertNotIn(
4331 222, self.services.spam.manual_verdicts_by_issue_id[78901])
4332
4333 def testFlagIssue_AutoVerdict(self):
4334 """Admins can mark and unmark an issue as spam and it counts as verdict."""
4335 self.SignIn(user_id=self.admin_user.user_id)
4336 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
4337 self.services.issue.TestAddIssue(issue)
4338 with self.work_env as we:
4339 we.FlagIssues([issue], True)
4340 self.assertEqual(
4341 [444], self.services.spam.reports_by_issue_id[78901])
4342 self.assertTrue(self.services.spam.manual_verdicts_by_issue_id[78901][444])
4343 with self.work_env as we:
4344 we.FlagIssues([issue], False)
4345 self.assertEqual(
4346 [], self.services.spam.reports_by_issue_id[78901])
4347 self.assertFalse(
4348 self.services.spam.manual_verdicts_by_issue_id[78901][444])
4349
4350 def testFlagIssue_NotAllowed(self):
4351 """Anons can't mark issues as spam."""
4352 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
4353 self.services.issue.TestAddIssue(issue)
4354
4355 with self.assertRaises(permissions.PermissionException):
4356 with self.work_env as we:
4357 we.FlagIssues([issue], True)
4358
4359 with self.assertRaises(permissions.PermissionException):
4360 with self.work_env as we:
4361 we.FlagIssues([issue], False)
4362
4363 def testLookupIssuesFlaggers_Normal(self):
4364 issue_1 = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
4365 self.services.issue.TestAddIssue(issue_1)
4366 comment_1_1 = tracker_pb2.IssueComment(
4367 project_id=789, content='lorem ipsum', user_id=111,
4368 issue_id=issue_1.issue_id)
4369 comment_1_2 = tracker_pb2.IssueComment(
4370 project_id=789, content='dolor sit amet', user_id=111,
4371 issue_id=issue_1.issue_id)
4372 self.services.issue.TestAddComment(comment_1_1, 1)
4373 self.services.issue.TestAddComment(comment_1_2, 1)
4374
4375 issue_2 = fake.MakeTestIssue(789, 2, 'sum', 'New', 111, issue_id=78902)
4376 self.services.issue.TestAddIssue(issue_2)
4377 comment_2_1 = tracker_pb2.IssueComment(
4378 project_id=789, content='lorem ipsum', user_id=111,
4379 issue_id=issue_2.issue_id)
4380 self.services.issue.TestAddComment(comment_2_1, 2)
4381
4382
4383 self.SignIn(user_id=222)
4384 with self.work_env as we:
4385 we.FlagIssues([issue_1], True)
4386
4387 self.SignIn(user_id=111)
4388 with self.work_env as we:
4389 we.FlagComment(issue_1, comment_1_2, True)
4390 we.FlagComment(issue_2, comment_2_1, True)
4391
4392 reporters = we.LookupIssuesFlaggers([issue_1, issue_2])
4393 self.assertEqual({
4394 issue_1.issue_id: ([222], {comment_1_2.id: [111]}),
4395 issue_2.issue_id: ([], {comment_2_1.id: [111]}),
4396 }, reporters)
4397
4398 def testLookupIssueFlaggers_Normal(self):
4399 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
4400 self.services.issue.TestAddIssue(issue)
4401 comment_1 = tracker_pb2.IssueComment(
4402 project_id=789, content='lorem ipsum', user_id=111,
4403 issue_id=issue.issue_id)
4404 comment_2 = tracker_pb2.IssueComment(
4405 project_id=789, content='dolor sit amet', user_id=111,
4406 issue_id=issue.issue_id)
4407 self.services.issue.TestAddComment(comment_1, 1)
4408 self.services.issue.TestAddComment(comment_2, 2)
4409
4410 self.SignIn(user_id=222)
4411 with self.work_env as we:
4412 we.FlagIssues([issue], True)
4413
4414 self.SignIn(user_id=111)
4415 with self.work_env as we:
4416 we.FlagComment(issue, comment_2, True)
4417 issue_reporters, comment_reporters = we.LookupIssueFlaggers(issue)
4418 self.assertEqual([222], issue_reporters)
4419 self.assertEqual({comment_2.id: [111]}, comment_reporters)
4420
4421 def testGetIssuePositionInHotlist(self):
4422 issue1 = fake.MakeTestIssue(
4423 789, 1, 'sum1', 'New', self.user_1.user_id, issue_id=78901)
4424 self.services.issue.TestAddIssue(issue1)
4425 issue2 = fake.MakeTestIssue(
4426 789, 2, 'sum1', 'New', self.user_2.user_id, issue_id=78902)
4427 self.services.issue.TestAddIssue(issue2)
4428 issue3 = fake.MakeTestIssue(
4429 789, 3, 'sum1', 'New', self.user_3.user_id, issue_id=78903)
4430 self.services.issue.TestAddIssue(issue3)
4431
4432 hotlist = self.work_env.services.features.CreateHotlist(
4433 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
4434 owner_ids=[self.user_1.user_id], editor_ids=[])
4435 self.AddIssueToHotlist(hotlist.hotlist_id, issue_id=issue2.issue_id)
4436 self.AddIssueToHotlist(hotlist.hotlist_id, issue_id=issue1.issue_id)
4437 self.AddIssueToHotlist(hotlist.hotlist_id, issue_id=issue3.issue_id)
4438
4439 with self.work_env as we:
4440 prev_iid, cur_index, next_iid, total_count = we.GetIssuePositionInHotlist(
4441 issue1, hotlist, 1, 'rank', '')
4442
4443 self.assertEqual(prev_iid, issue2.issue_id)
4444 self.assertEqual(cur_index, 1)
4445 self.assertEqual(next_iid, issue3.issue_id)
4446 self.assertEqual(total_count, 3)
4447
4448 def testRerankBlockedOnIssues_SplitBelow(self):
4449 parent_issue = fake.MakeTestIssue(
4450 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
4451 self.services.issue.TestAddIssue(parent_issue)
4452
4453 issues = []
4454 for idx in range(2, 6):
4455 issues.append(fake.MakeTestIssue(
4456 789, idx, 'sum', 'New', 111, project_name='proj', issue_id=1000+idx))
4457 self.services.issue.TestAddIssue(issues[-1])
4458 parent_issue.blocked_on_iids.append(issues[-1].issue_id)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01004459 next_rank = sys.maxsize
Copybara854996b2021-09-07 19:36:02 +00004460 if parent_issue.blocked_on_ranks:
4461 next_rank = parent_issue.blocked_on_ranks[-1] - 1
4462 parent_issue.blocked_on_ranks.append(next_rank)
4463
4464 self.SignIn()
4465 with self.work_env as we:
4466 we.RerankBlockedOnIssues(parent_issue, 1002, 1004, False)
4467 new_parent_issue = we.GetIssue(1001)
4468
4469 self.assertEqual([1003, 1004, 1002, 1005], new_parent_issue.blocked_on_iids)
4470
4471 def testRerankBlockedOnIssues_SplitAbove(self):
4472 parent_issue = fake.MakeTestIssue(
4473 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
4474 self.services.issue.TestAddIssue(parent_issue)
4475
4476 issues = []
4477 for idx in range(2, 6):
4478 issues.append(fake.MakeTestIssue(
4479 789, idx, 'sum', 'New', 111, project_name='proj', issue_id=1000+idx))
4480 self.services.issue.TestAddIssue(issues[-1])
4481 parent_issue.blocked_on_iids.append(issues[-1].issue_id)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01004482 next_rank = sys.maxsize
Copybara854996b2021-09-07 19:36:02 +00004483 if parent_issue.blocked_on_ranks:
4484 next_rank = parent_issue.blocked_on_ranks[-1] - 1
4485 parent_issue.blocked_on_ranks.append(next_rank)
4486
4487 self.SignIn()
4488 with self.work_env as we:
4489 we.RerankBlockedOnIssues(parent_issue, 1002, 1004, True)
4490 new_parent_issue = we.GetIssue(1001)
4491
4492 self.assertEqual([1003, 1002, 1004, 1005], new_parent_issue.blocked_on_iids)
4493
4494 @mock.patch('tracker.rerank_helpers.MAX_RANKING', 1)
4495 def testRerankBlockedOnIssues_NoRoom(self):
4496 parent_issue = fake.MakeTestIssue(
4497 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
4498 parent_issue.blocked_on_ranks = [1, 0, 0]
4499 self.services.issue.TestAddIssue(parent_issue)
4500
4501 issues = []
4502 for idx in range(2, 5):
4503 issues.append(fake.MakeTestIssue(
4504 789, idx, 'sum', 'New', 111, project_name='proj', issue_id=1000+idx))
4505 self.services.issue.TestAddIssue(issues[-1])
4506 parent_issue.blocked_on_iids.append(issues[-1].issue_id)
4507
4508 self.SignIn()
4509 with self.work_env as we:
4510 we.RerankBlockedOnIssues(parent_issue, 1003, 1004, True)
4511 new_parent_issue = we.GetIssue(1001)
4512
4513 self.assertEqual([1002, 1003, 1004], new_parent_issue.blocked_on_iids)
4514
4515 def testRerankBlockedOnIssues_CantEditIssue(self):
4516 parent_issue = fake.MakeTestIssue(
4517 789, 1, 'sum', 'New', 555, project_name='proj', issue_id=1001)
4518 parent_issue.labels = ['Restrict-EditIssue-Foo']
4519 self.services.issue.TestAddIssue(parent_issue)
4520
4521 self.SignIn()
4522 with self.assertRaises(permissions.PermissionException):
4523 with self.work_env as we:
4524 we.RerankBlockedOnIssues(parent_issue, 1003, 1002, True)
4525
4526 def testRerankBlockedOnIssues_MovedNotOnBlockedOn(self):
4527 parent_issue = fake.MakeTestIssue(
4528 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
4529 self.services.issue.TestAddIssue(parent_issue)
4530
4531 self.SignIn()
4532 with self.assertRaises(exceptions.InputException):
4533 with self.work_env as we:
4534 we.RerankBlockedOnIssues(parent_issue, 1003, 1002, True)
4535
4536 def testRerankBlockedOnIssues_TargetNotOnBlockedOn(self):
4537 moved = fake.MakeTestIssue(
4538 789, 2, 'sum', 'New', 111, project_name='proj', issue_id=1002)
4539 self.services.issue.TestAddIssue(moved)
4540 parent_issue = fake.MakeTestIssue(
4541 789, 1, 'sum', 'New', 111, project_name='proj', issue_id=1001)
4542 parent_issue.blocked_on_iids = [1002]
4543 parent_issue.blocked_on_ranks = [1]
4544 self.services.issue.TestAddIssue(parent_issue)
4545
4546 self.SignIn()
4547 with self.assertRaises(exceptions.InputException):
4548 with self.work_env as we:
4549 we.RerankBlockedOnIssues(parent_issue, 1002, 1003, True)
4550
4551 # FUTURE: GetIssuePermissionsForUser()
4552
4553 # FUTURE: CreateComment()
4554
4555 def testListIssueComments_Normal(self):
4556 """We can list comments for an issue."""
4557 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
4558 self.services.issue.TestAddIssue(issue)
4559 comment = tracker_pb2.IssueComment(
4560 project_id=789, content='more info', user_id=111,
4561 issue_id=issue.issue_id)
4562 self.services.issue.TestAddComment(comment, 1)
4563
4564 with self.work_env as we:
4565 actual_comments = we.ListIssueComments(issue)
4566
4567 self.assertEqual(2, len(actual_comments))
4568 self.assertEqual('sum', actual_comments[0].content)
4569 self.assertEqual('more info', actual_comments[1].content)
4570
4571 def _Comment(self, issue, content, local_id, approval_id=None):
4572 """Adds a comment to issue with reasonable defaults."""
4573 comment = tracker_pb2.IssueComment(
4574 project_id=issue.project_id,
4575 content=content,
4576 user_id=issue.reporter_id,
4577 issue_id=issue.issue_id,
4578 approval_id=approval_id)
4579 self.services.issue.TestAddComment(comment, local_id)
4580
4581 def testSafeListIssueComments_Normal(self):
4582 initial_description = 'sum'
4583 issue = fake.MakeTestIssue(
4584 self.project.project_id,
4585 1,
4586 initial_description,
4587 'New',
4588 self.user_1.user_id,
4589 issue_id=78901,
4590 project_name=self.project.project_name)
4591 self.services.issue.TestAddIssue(issue)
4592 self._Comment(issue, 'more info', 1)
4593
4594 with self.work_env as we:
4595 list_result = we.SafeListIssueComments(issue.issue_id, 1000, 0)
4596
4597 self.assertEqual(None, list_result.next_start)
4598 actual_comments = list_result.items
4599 self.assertEqual(2, len(actual_comments))
4600 self.assertEqual(initial_description, actual_comments[0].content)
4601 self.assertEqual('more info', actual_comments[1].content)
4602
4603
4604 def testSafeListIssueComments_DeletedIssue(self):
4605 """Users without permissions cannot view comments on deleted issues."""
4606 issue = fake.MakeTestIssue(
4607 self.project.project_id,
4608 1,
4609 'sum',
4610 'New',
4611 self.user_1.user_id,
4612 issue_id=78901,
4613 project_name=self.project.project_name)
4614 issue.deleted = True
4615 self.services.issue.TestAddIssue(issue)
4616 with self.assertRaises(permissions.PermissionException):
4617 with self.work_env as we:
4618 we.SafeListIssueComments(issue.issue_id, 1000, 0)
4619
4620 def testSafeListIssueComments_NotAllowed(self):
4621 issue = fake.MakeTestIssue(
4622 self.project.project_id,
4623 1,
4624 'sum',
4625 'New',
4626 self.user_1.user_id,
4627 issue_id=78901,
4628 project_name=self.project.project_name,
4629 labels=['Restrict-View-CoreTeam'])
4630 self.services.issue.TestAddIssue(issue)
4631
4632 with self.assertRaises(permissions.PermissionException):
4633 with self.work_env as we:
4634 we.SafeListIssueComments(issue.issue_id, 1000, 0)
4635
4636 def testSafeListIssueComments_UserFlagged(self):
4637 """Users see comments they flagged as spam."""
4638 issue = fake.MakeTestIssue(
4639 self.project.project_id,
4640 1,
4641 'sum',
4642 'New',
4643 self.user_1.user_id,
4644 issue_id=78901,
4645 project_name=self.project.project_name)
4646 self.services.issue.TestAddIssue(issue)
4647 flagged_comment = tracker_pb2.IssueComment(
4648 project_id=self.project.project_id,
4649 content='flagged content',
4650 user_id=self.user_1.user_id,
4651 issue_id=issue.issue_id,
4652 inbound_message='Some message',
4653 importer_id=self.user_1.user_id)
4654 self.services.issue.TestAddComment(flagged_comment, 1)
4655
4656 self.services.spam.FlagComment(
4657 self.cnxn, issue, flagged_comment.id, flagged_comment.user_id,
4658 self.user_2.user_id, True)
4659
4660 # One user flagging a comment doesn't cause other users to see it as spam.
4661 with self.work_env as we:
4662 list_result = we.SafeListIssueComments(issue.issue_id, 1000, 0)
4663 self.assertFalse(list_result.items[1].is_spam)
4664
4665 self.SignIn(self.user_2.user_id)
4666 with self.work_env as we:
4667 list_result = we.SafeListIssueComments(issue.issue_id, 1000, 0)
4668 self.assertTrue(list_result.items[1].is_spam)
4669 self.assertEqual('flagged content', list_result.items[1].content)
4670
4671 def testSafeListIssueComments_FilteredContent(self):
4672
4673 def AssertFiltered(comment, filtered_comment):
4674 # Unfiltered
4675 self.assertEqual(comment.id, filtered_comment.id)
4676 self.assertEqual(comment.issue_id, filtered_comment.issue_id)
4677 self.assertEqual(comment.project_id, filtered_comment.project_id)
4678 self.assertEqual(comment.approval_id, filtered_comment.approval_id)
4679 self.assertEqual(comment.timestamp, filtered_comment.timestamp)
4680 self.assertEqual(comment.deleted_by, filtered_comment.deleted_by)
4681 self.assertEqual(comment.sequence, filtered_comment.sequence)
4682 self.assertEqual(comment.is_spam, filtered_comment.is_spam)
4683 self.assertEqual(comment.is_description, filtered_comment.is_description)
4684 self.assertEqual(
4685 comment.description_num, filtered_comment.description_num)
4686 # Filtered.
4687 self.assertEqual(None, filtered_comment.content)
4688 self.assertEqual(0, filtered_comment.user_id)
4689 self.assertEqual([], filtered_comment.amendments)
4690 self.assertEqual([], filtered_comment.attachments)
4691 self.assertEqual(None, filtered_comment.inbound_message)
4692 self.assertEqual(0, filtered_comment.importer_id)
4693
4694 initial_description = 'sum'
4695 issue = fake.MakeTestIssue(
4696 self.project.project_id,
4697 1,
4698 initial_description,
4699 'New',
4700 self.user_1.user_id,
4701 issue_id=78901,
4702 project_name=self.project.project_name)
4703 self.services.issue.TestAddIssue(issue)
4704 spam_comment = tracker_pb2.IssueComment(
4705 project_id=self.project.project_id,
4706 content='spam',
4707 user_id=self.user_1.user_id,
4708 issue_id=issue.issue_id,
4709 is_spam=True,
4710 inbound_message='Some message',
4711 importer_id=self.user_1.user_id)
4712 deleted_comment = tracker_pb2.IssueComment(
4713 project_id=self.project.project_id,
4714 content='deleted',
4715 user_id=self.user_1.user_id,
4716 issue_id=issue.issue_id,
4717 deleted_by=self.user_1.user_id,
4718 amendments=[
4719 tracker_pb2.Amendment(
4720 field=tracker_pb2.FieldID.SUMMARY, newvalue='new')
4721 ],
4722 attachments=[
4723 tracker_pb2.Attachment(
4724 attachment_id=1,
4725 mimetype='image/png',
4726 filename='example.png',
4727 filesize=12345)
4728 ])
4729 inbound_comment = tracker_pb2.IssueComment(
4730 project_id=self.project.project_id,
4731 content='from an inbound message',
4732 user_id=self.user_1.user_id,
4733 issue_id=issue.issue_id,
4734 inbound_message='the full inbound message')
4735 self.services.issue.TestAddComment(spam_comment, 1)
4736 self.services.issue.TestAddComment(deleted_comment, 2)
4737 self.services.issue.TestAddComment(inbound_comment, 3)
4738 with self.work_env as we:
4739 list_result = we.SafeListIssueComments(issue.issue_id, 1000, 0)
4740
4741 self.assertEqual(None, list_result.next_start)
4742 actual_comments = list_result.items
4743 self.assertEqual(4, len(actual_comments))
4744 self.assertEqual(initial_description, actual_comments[0].content)
4745 AssertFiltered(spam_comment, actual_comments[1])
4746 AssertFiltered(deleted_comment, actual_comments[2])
4747 self.assertEqual('from an inbound message', actual_comments[3].content)
4748 self.assertEqual(None, actual_comments[3].inbound_message)
4749
4750 def testSafeListIssueComments_AdminsViewUnfiltered(self):
4751 """Admins can appropriately view comment content that would be filtered."""
4752 issue = fake.MakeTestIssue(
4753 self.project.project_id,
4754 1,
4755 'sum',
4756 'New',
4757 self.user_1.user_id,
4758 issue_id=78901,
4759 project_name=self.project.project_name)
4760 self.services.issue.TestAddIssue(issue)
4761 spam_comment = tracker_pb2.IssueComment(
4762 project_id=self.project.project_id,
4763 content='spam',
4764 user_id=self.user_1.user_id,
4765 issue_id=issue.issue_id,
4766 is_spam=True,
4767 inbound_message='Some message',
4768 importer_id=self.user_1.user_id)
4769 deleted_comment = tracker_pb2.IssueComment(
4770 project_id=self.project.project_id,
4771 content='deleted',
4772 user_id=self.user_1.user_id,
4773 issue_id=issue.issue_id,
4774 deleted_by=self.user_1.user_id,
4775 amendments=[
4776 tracker_pb2.Amendment(
4777 field=tracker_pb2.FieldID.SUMMARY, newvalue='new')
4778 ],
4779 attachments=[
4780 tracker_pb2.Attachment(
4781 attachment_id=1,
4782 mimetype='image/png',
4783 filename='example.png',
4784 filesize=12345)
4785 ])
4786 inbound_comment = tracker_pb2.IssueComment(
4787 project_id=self.project.project_id,
4788 content='from an inbound message',
4789 user_id=self.user_1.user_id,
4790 issue_id=issue.issue_id,
4791 inbound_message='the full inbound message')
4792 self.services.issue.TestAddComment(spam_comment, 1)
4793 self.services.issue.TestAddComment(deleted_comment, 2)
4794 self.services.issue.TestAddComment(inbound_comment, 3)
4795
4796 self.SignIn(self.admin_user.user_id)
4797 with self.work_env as we:
4798 list_result = we.SafeListIssueComments(issue.issue_id, 1000, 0)
4799
4800 # Admins can view the fields of comments that would be filtered.
4801 actual_comments = list_result.items
4802 self.assertEqual(spam_comment.content, actual_comments[1].content)
4803 self.assertEqual(deleted_comment.content, actual_comments[2].content)
4804 self.assertEqual(
4805 'the full inbound message', actual_comments[3].inbound_message)
4806
4807 def testSafeListIssueComments_MoreItems(self):
4808 initial_description = 'sum'
4809 issue = fake.MakeTestIssue(
4810 self.project.project_id,
4811 1,
4812 initial_description,
4813 'New',
4814 self.user_1.user_id,
4815 issue_id=78901,
4816 project_name=self.project.project_name)
4817 self.services.issue.TestAddIssue(issue)
4818 self._Comment(issue, 'more info', 1)
4819
4820 with self.work_env as we:
4821 list_result = we.SafeListIssueComments(issue.issue_id, 1, 0)
4822
4823 self.assertEqual(1, list_result.next_start)
4824 actual_comments = list_result.items
4825 self.assertEqual(1, len(actual_comments))
4826 self.assertEqual(initial_description, actual_comments[0].content)
4827
4828 def testSafeListIssueComments_Start(self):
4829 initial_description = 'sum'
4830 issue = fake.MakeTestIssue(
4831 self.project.project_id,
4832 1,
4833 initial_description,
4834 'New',
4835 self.user_1.user_id,
4836 issue_id=78901,
4837 project_name=self.project.project_name)
4838 self.services.issue.TestAddIssue(issue)
4839 self._Comment(issue, 'more info', 1)
4840
4841 with self.work_env as we:
4842 list_result = we.SafeListIssueComments(issue.issue_id, 1000, 1)
4843 self.assertEqual(None, list_result.next_start)
4844 actual_comments = list_result.items
4845 self.assertEqual(1, len(actual_comments))
4846 self.assertEqual('more info', actual_comments[0].content)
4847
4848 def testSafeListIssueComments_ApprovalId(self):
4849 issue = fake.MakeTestIssue(
4850 self.project.project_id,
4851 1,
4852 'initial description',
4853 'New',
4854 self.user_1.user_id,
4855 issue_id=78901,
4856 project_name=self.project.project_name)
4857 self.services.issue.TestAddIssue(issue)
4858
4859 max_items = 2
4860 # Create comments for testing.
4861 self._Comment(issue, 'more info', 1)
4862 self._Comment(issue, 'approval2 info', 2, approval_id=2)
4863 # This would be after the max_items of 2, so we are ensuring that the
4864 # max_items limit applies AFTER filtering rather than before.
4865 self._Comment(issue, 'approval1 info1', 3, approval_id=1)
4866 self._Comment(issue, 'approval1 info2', 4, approval_id=1)
4867 self._Comment(issue, 'approval1 info3', 5, approval_id=1)
4868
4869 with self.work_env as we:
4870 list_result = we.SafeListIssueComments(
4871 issue.issue_id, max_items, 0, approval_id=1)
4872 self.assertEqual(
4873 2, list_result.next_start, 'We have a third approval comment')
4874 actual_comments = list_result.items
4875 self.assertEqual(2, len(actual_comments))
4876 self.assertEqual('approval1 info1', actual_comments[0].content)
4877 self.assertEqual('approval1 info2', actual_comments[1].content)
4878
4879 def testSafeListIssueComments_StartAndApprovalId(self):
4880 issue = fake.MakeTestIssue(
4881 self.project.project_id,
4882 1,
4883 'initial description',
4884 'New',
4885 self.user_1.user_id,
4886 issue_id=78901,
4887 project_name=self.project.project_name)
4888 self.services.issue.TestAddIssue(issue)
4889
4890 # Create comments for testing.
4891 self._Comment(issue, 'more info', 1)
4892 self._Comment(issue, 'approval2 info', 2, approval_id=2)
4893 self._Comment(issue, 'approval1 info1', 3, approval_id=1)
4894 self._Comment(issue, 'approval1 info2', 4, approval_id=1)
4895 self._Comment(issue, 'approval1 info3', 5, approval_id=1)
4896
4897 with self.work_env as we:
4898 list_result = we.SafeListIssueComments(
4899 issue.issue_id, 1000, 1, approval_id=1)
4900 self.assertEqual(None, list_result.next_start)
4901 actual_comments = list_result.items
4902 self.assertEqual(2, len(actual_comments))
4903 self.assertEqual('approval1 info2', actual_comments[0].content)
4904 self.assertEqual('approval1 info3', actual_comments[1].content)
4905
4906 # FUTURE: UpdateComment()
4907
4908 def testDeleteComment_Normal(self):
4909 """We can mark and unmark a comment as deleted."""
4910 self.SignIn(user_id=111)
4911 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
4912 self.services.issue.TestAddIssue(issue)
4913 comment = tracker_pb2.IssueComment(
4914 project_id=789, content='soon to be deleted', user_id=111,
4915 issue_id=issue.issue_id)
4916 self.services.issue.TestAddComment(comment, 1)
4917 with self.work_env as we:
4918 we.DeleteComment(issue, comment, True)
4919 self.assertEqual(111, comment.deleted_by)
4920 we.DeleteComment(issue, comment, False)
4921 self.assertEqual(None, comment.deleted_by)
4922
4923 @mock.patch('services.issue_svc.IssueService.SoftDeleteComment')
4924 def testDeleteComment_UndeleteableSpam(self, mockSoftDeleteComment):
4925 """Throws exception when comment is spam and owner is deleting."""
4926 self.SignIn(user_id=111)
4927 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
4928 self.services.issue.TestAddIssue(issue)
4929 comment = tracker_pb2.IssueComment(
4930 project_id=789, content='soon to be deleted', user_id=111,
4931 issue_id=issue.issue_id, is_spam=True)
4932 self.services.issue.TestAddComment(comment, 1)
4933 with self.work_env as we:
4934 with self.assertRaises(permissions.PermissionException):
4935 we.DeleteComment(issue, comment, True)
4936 self.assertEqual(None, comment.deleted_by)
4937 mockSoftDeleteComment.assert_not_called()
4938
4939 @mock.patch('services.issue_svc.IssueService.SoftDeleteComment')
4940 @mock.patch('framework.permissions.CanDeleteComment')
4941 def testDeleteComment_UndeletablePermissions(self, mockCanDelete,
4942 mockSoftDeleteComment):
4943 """Throws exception when deleter doesn't have permission to do so."""
4944 mockCanDelete.return_value = False
4945 self.SignIn(user_id=111)
4946 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
4947 self.services.issue.TestAddIssue(issue)
4948 comment = tracker_pb2.IssueComment(
4949 project_id=789, content='soon to be deleted', user_id=111,
4950 issue_id=issue.issue_id, is_spam=True)
4951 self.services.issue.TestAddComment(comment, 1)
4952 with self.work_env as we:
4953 with self.assertRaises(permissions.PermissionException):
4954 we.DeleteComment(issue, comment, True)
4955 self.assertEqual(None, comment.deleted_by)
4956 mockSoftDeleteComment.assert_not_called()
4957
4958 def testDeleteAttachment_Normal(self):
4959 """We can mark and unmark a comment attachment as deleted."""
4960 self.SignIn(user_id=111)
4961 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
4962 self.services.issue.TestAddIssue(issue)
4963 comment = tracker_pb2.IssueComment(
4964 project_id=789, content='soon to be deleted', user_id=111,
4965 issue_id=issue.issue_id)
4966 self.services.issue.TestAddComment(comment, 1)
4967 attachment = tracker_pb2.Attachment()
4968 self.services.issue.TestAddAttachment(attachment, comment.id, 1)
4969 with self.work_env as we:
4970 we.DeleteAttachment(
4971 issue, comment, attachment.attachment_id, True)
4972 self.assertTrue(attachment.deleted)
4973 we.DeleteAttachment(
4974 issue, comment, attachment.attachment_id, False)
4975 self.assertFalse(attachment.deleted)
4976
4977 @mock.patch('services.issue_svc.IssueService.SoftDeleteComment')
4978 @mock.patch('framework.permissions.CanDeleteComment')
4979 def testDeleteAttachment_UndeletablePermissions(
4980 self, mockCanDelete, mockSoftDeleteComment):
4981 """Throws exception when deleter doesn't have permission to do so."""
4982 mockCanDelete.return_value = False
4983 self.SignIn(user_id=111)
4984 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
4985 self.services.issue.TestAddIssue(issue)
4986 comment = tracker_pb2.IssueComment(
4987 project_id=789, content='soon to be deleted', user_id=111,
4988 issue_id=issue.issue_id, is_spam=True)
4989 self.services.issue.TestAddComment(comment, 1)
4990 attachment = tracker_pb2.Attachment()
4991 self.services.issue.TestAddAttachment(attachment, comment.id, 1)
4992 self.assertFalse(attachment.deleted)
4993 with self.work_env as we:
4994 with self.assertRaises(permissions.PermissionException):
4995 we.DeleteAttachment(
4996 issue, comment, attachment.attachment_id, True)
4997 self.assertFalse(attachment.deleted)
4998 mockSoftDeleteComment.assert_not_called()
4999
5000 def testFlagComment_Normal(self):
5001 """We can mark and unmark a comment as spam."""
5002 self.SignIn(user_id=111)
5003 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
5004 self.services.issue.TestAddIssue(issue)
5005 comment = tracker_pb2.IssueComment(
5006 project_id=789, content='soon to be deleted', user_id=111,
5007 issue_id=issue.issue_id)
5008 self.services.issue.TestAddComment(comment, 1)
5009
5010 comment_reports = self.services.spam.comment_reports_by_issue_id
5011 with self.work_env as we:
5012 we.FlagComment(issue, comment, True)
5013 self.assertEqual([111], comment_reports[issue.issue_id][comment.id])
5014 we.FlagComment(issue, comment, False)
5015 self.assertEqual([], comment_reports[issue.issue_id][comment.id])
5016
5017 def testFlagComment_AutoVerdict(self):
5018 """Admins can mark and unmark a comment as spam, and it is a verdict."""
5019 self.SignIn(user_id=self.admin_user.user_id)
5020 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
5021 self.services.issue.TestAddIssue(issue)
5022 comment = tracker_pb2.IssueComment(
5023 project_id=789, content='soon to be deleted', user_id=111,
5024 issue_id=issue.issue_id)
5025 self.services.issue.TestAddComment(comment, 1)
5026
5027 comment_reports = self.services.spam.comment_reports_by_issue_id
5028 manual_verdicts = self.services.spam.manual_verdicts_by_comment_id
5029 with self.work_env as we:
5030 we.FlagComment(issue, comment, True)
5031 self.assertEqual([444], comment_reports[issue.issue_id][comment.id])
5032 self.assertTrue(manual_verdicts[comment.id][444])
5033 we.FlagComment(issue, comment, False)
5034 self.assertEqual([], comment_reports[issue.issue_id][comment.id])
5035 self.assertFalse(manual_verdicts[comment.id][444])
5036
5037 def testFlagComment_NotAllowed(self):
5038 """Anons can't mark comment as spam."""
5039 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
5040 self.services.issue.TestAddIssue(issue)
5041 comment = tracker_pb2.IssueComment(
5042 project_id=789, content='soon to be deleted', user_id=111,
5043 issue_id=issue.issue_id)
5044 self.services.issue.TestAddComment(comment, 1)
5045
5046 with self.assertRaises(permissions.PermissionException):
5047 with self.work_env as we:
5048 we.FlagComment(issue, comment, True)
5049
5050 with self.assertRaises(permissions.PermissionException):
5051 with self.work_env as we:
5052 we.FlagComment(issue, comment, False)
5053
5054 def testStarIssue_Normal(self):
5055 """We can star and unstar issues."""
5056 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
5057 self.services.issue.TestAddIssue(issue)
5058 self.SignIn(user_id=111)
5059
5060 with self.work_env as we:
5061 updated_issue = we.StarIssue(issue, True)
5062 self.assertEqual(1, updated_issue.star_count)
5063 updated_issue = we.StarIssue(issue, False)
5064 self.assertEqual(0, updated_issue.star_count)
5065
5066 def testStarIssue_Anon(self):
5067 """A signed out user cannot star or unstar issues."""
5068 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
5069 self.services.issue.TestAddIssue(issue)
5070 # Don't sign in.
5071
5072 with self.assertRaises(permissions.PermissionException):
5073 with self.work_env as we:
5074 we.StarIssue(issue, True)
5075
5076 def testIsIssueStarred_Normal(self):
5077 """We can check if the current user starred an issue or not."""
5078 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
5079 self.services.issue.TestAddIssue(issue)
5080 self.SignIn(user_id=111)
5081
5082 with self.work_env as we:
5083 self.assertFalse(we.IsIssueStarred(issue))
5084 we.StarIssue(issue, True)
5085 self.assertTrue(we.IsIssueStarred(issue))
5086 we.StarIssue(issue, False)
5087 self.assertFalse(we.IsIssueStarred(issue))
5088
5089 def testIsIssueStarred_Anon(self):
5090 """A signed out user has never starred anything."""
5091 issue = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
5092 self.services.issue.TestAddIssue(issue)
5093 # Don't sign in.
5094
5095 with self.work_env as we:
5096 self.assertFalse(we.IsIssueStarred(issue))
5097
5098 def testListStarredIssueIDs_Anon(self):
5099 """A signed out users has no starred issues."""
5100 # Don't sign in.
5101 with self.work_env as we:
5102 self.assertEqual([], we.ListStarredIssueIDs())
5103
5104 def testListStarredIssueIDs_Normal(self):
5105 """We can get the list of issues starred by a user."""
5106 issue1 = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
5107 self.services.issue.TestAddIssue(issue1)
5108 issue2 = fake.MakeTestIssue(789, 2, 'sum2', 'New', 111, issue_id=78902)
5109 self.services.issue.TestAddIssue(issue2)
5110
5111 self.SignIn(user_id=111)
5112 with self.work_env as we:
5113 # User has not starred anything yet.
5114 self.assertEqual([], we.ListStarredIssueIDs())
5115
5116 # Now, star a couple of issues.
5117 we.StarIssue(issue1, True)
5118 we.StarIssue(issue2, True)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005119 six.assertCountEqual(
5120 self, [issue1.issue_id, issue2.issue_id], we.ListStarredIssueIDs())
Copybara854996b2021-09-07 19:36:02 +00005121
5122 # Check that there is no cross-talk between users.
5123 self.SignIn(user_id=222)
5124 with self.work_env as we:
5125 # User has not starred anything yet.
5126 self.assertEqual([], we.ListStarredIssueIDs())
5127
5128 # Now, star an issue as that other user.
5129 we.StarIssue(issue1, True)
5130 self.assertEqual([issue1.issue_id], we.ListStarredIssueIDs())
5131
5132 def testGetUser(self):
5133 """We return the User PB for the given existing user id."""
5134 expected = self.services.user.TestAddUser('test5@example.com', 555)
5135 with self.work_env as we:
5136 actual = we.GetUser(555)
5137 self.assertEqual(expected, actual)
5138
5139 def testBatchGetUsers(self):
5140 """We return the User PBs for all given user ids."""
5141 actual = self.work_env.BatchGetUsers(
5142 [self.user_1.user_id, self.user_2.user_id])
5143 self.assertEqual(actual, [self.user_1, self.user_2])
5144
5145 def testBatchGetUsers_NoUserFound(self):
5146 """We raise an exception if a User is not found."""
5147 with self.assertRaises(exceptions.NoSuchUserException):
5148 self.work_env.BatchGetUsers(
5149 [self.user_1.user_id, self.user_2.user_id, 404])
5150
5151 def testGetUser_DoesntExist(self):
5152 """We reject attempts to get an user that doesn't exist."""
5153 with self.assertRaises(exceptions.NoSuchUserException):
5154 with self.work_env as we:
5155 we.GetUser(555)
5156
5157 def setUpUserGroups(self):
5158 self.services.user.TestAddUser('test5@example.com', 555)
5159 self.services.user.TestAddUser('test6@example.com', 666)
5160 public_group_id = self.services.usergroup.CreateGroup(
5161 self.cnxn, self.services, 'group1@test.com', 'anyone')
5162 private_group_id = self.services.usergroup.CreateGroup(
5163 self.cnxn, self.services, 'group2@test.com', 'owners')
5164 self.services.usergroup.UpdateMembers(
5165 self.cnxn, public_group_id, [111], 'member')
5166 self.services.usergroup.UpdateMembers(
5167 self.cnxn, private_group_id, [555, 111], 'owner')
5168 return public_group_id, private_group_id
5169
5170 def testGetMemberships_Anon(self):
5171 """We return groups the user is in and that are visible to the requester."""
5172 public_group_id, _ = self.setUpUserGroups()
5173 with self.work_env as we:
5174 self.assertEqual(we.GetMemberships(111), [public_group_id])
5175
5176 def testGetMemberships_UserHasPerm(self):
5177 public_group_id, private_group_id = self.setUpUserGroups()
5178 self.SignIn(user_id=555)
5179 with self.work_env as we:
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005180 six.assertCountEqual(
5181 self, we.GetMemberships(111), [public_group_id, private_group_id])
Copybara854996b2021-09-07 19:36:02 +00005182
5183 def testGetMemeberships_UserHasNoPerm(self):
5184 public_group_id, _ = self.setUpUserGroups()
5185 self.SignIn(user_id=666)
5186 with self.work_env as we:
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005187 six.assertCountEqual(self, we.GetMemberships(111), [public_group_id])
Copybara854996b2021-09-07 19:36:02 +00005188
5189 def testGetMemeberships_GetOwnMembership(self):
5190 public_group_id, private_group_id = self.setUpUserGroups()
5191 self.SignIn(user_id=111)
5192 with self.work_env as we:
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005193 six.assertCountEqual(
5194 self, we.GetMemberships(111), [public_group_id, private_group_id])
Copybara854996b2021-09-07 19:36:02 +00005195
5196 def testListReferencedUsers(self):
5197 """We return the list of User PBs for the given existing user emails."""
5198 user5 = self.services.user.TestAddUser('test5@example.com', 555)
5199 user6 = self.services.user.TestAddUser('test6@example.com', 666)
5200 with self.work_env as we:
5201 # We ignore emails that are empty or belong to non-existent users.
5202 users, linked_user_ids = we.ListReferencedUsers(
5203 ['test4@example.com', 'test5@example.com', 'test6@example.com', ''])
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005204 six.assertCountEqual(self, users, [user5, user6])
Copybara854996b2021-09-07 19:36:02 +00005205 self.assertEqual(linked_user_ids, [])
5206
5207 def testListReferencedUsers_Linked(self):
5208 """We return User PBs and the IDs of any linked accounts."""
5209 user5 = self.services.user.TestAddUser('test5@example.com', 555)
5210 user5.linked_child_ids = [666, 777]
5211 user6 = self.services.user.TestAddUser('test6@example.com', 666)
5212 user6.linked_parent_id = 555
5213 with self.work_env as we:
5214 # We ignore emails that are empty or belong to non-existent users.
5215 users, linked_user_ids = we.ListReferencedUsers(
5216 ['test4@example.com', 'test5@example.com', 'test6@example.com', ''])
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005217 six.assertCountEqual(self, users, [user5, user6])
5218 six.assertCountEqual(self, linked_user_ids, [555, 666, 777])
Copybara854996b2021-09-07 19:36:02 +00005219
5220 def testStarUser_Normal(self):
5221 """We can star and unstar a user."""
5222 self.SignIn()
5223 with self.work_env as we:
5224 self.assertFalse(we.IsUserStarred(111))
5225 we.StarUser(111, True)
5226 self.assertTrue(we.IsUserStarred(111))
5227 we.StarUser(111, False)
5228 self.assertFalse(we.IsUserStarred(111))
5229
5230 def testStarUser_NoSuchUser(self):
5231 """We can't star a nonexistent user."""
5232 self.SignIn()
5233 with self.assertRaises(exceptions.NoSuchUserException):
5234 with self.work_env as we:
5235 we.StarUser(999, True)
5236
5237 def testStarUser_Anon(self):
5238 """Anon user can't star a user."""
5239 with self.assertRaises(exceptions.InputException):
5240 with self.work_env as we:
5241 we.StarUser(111, True)
5242
5243 def testIsUserStarred_Normal(self):
5244 """We can check if a user is starred."""
5245 # Tested by method testStarUser_Normal().
5246 pass
5247
5248 def testIsUserStarred_NoUserSpecified(self):
5249 """A user ID must be specified."""
5250 with self.work_env as we:
5251 with self.assertRaises(exceptions.InputException):
5252 self.assertFalse(we.IsUserStarred(None))
5253
5254 def testIsUserStarred_NoSuchUser(self):
5255 """We can't check for stars on a nonexistent user."""
5256 self.SignIn()
5257 with self.assertRaises(exceptions.NoSuchUserException):
5258 with self.work_env as we:
5259 we.IsUserStarred(999)
5260
5261 def testGetUserStarCount_Normal(self):
5262 """We can count the stars of a user."""
5263 self.SignIn()
5264 with self.work_env as we:
5265 self.assertEqual(0, we.GetUserStarCount(111))
5266 we.StarUser(111, True)
5267 self.assertEqual(1, we.GetUserStarCount(111))
5268
5269 self.SignIn(user_id=self.admin_user.user_id)
5270 with self.work_env as we:
5271 we.StarUser(111, True)
5272 self.assertEqual(2, we.GetUserStarCount(111))
5273 we.StarUser(111, False)
5274 self.assertEqual(1, we.GetUserStarCount(111))
5275
5276 def testGetUserStarCount_NoSuchUser(self):
5277 """We can't count stars of a nonexistent user."""
5278 self.SignIn()
5279 with self.assertRaises(exceptions.NoSuchUserException):
5280 with self.work_env as we:
5281 we.GetUserStarCount(111111)
5282
5283 def testGetUserStarCount_NoUserSpecified(self):
5284 """A user ID must be specified."""
5285 with self.work_env as we:
5286 with self.assertRaises(exceptions.InputException):
5287 self.assertFalse(we.GetUserStarCount(None))
5288
5289 def testGetPendingLinkInvites_Anon(self):
5290 """Anon never had pending linkage invites."""
5291 with self.work_env as we:
5292 as_parent, as_child = we.GetPendingLinkedInvites()
5293 self.assertEqual([], as_parent)
5294 self.assertEqual([], as_child)
5295
5296 def testGetPendingLinkInvites_None(self):
5297 """When an account has no invites, we see empty lists."""
5298 self.SignIn()
5299 with self.work_env as we:
5300 as_parent, as_child = we.GetPendingLinkedInvites()
5301 self.assertEqual([], as_parent)
5302 self.assertEqual([], as_child)
5303
5304 def testGetPendingLinkInvites_Some(self):
5305 """If there are any pending invites for the current user, we get them."""
5306 self.SignIn()
5307 self.services.user.invite_rows = [(111, 222), (333, 444), (555, 111)]
5308 with self.work_env as we:
5309 as_parent, as_child = we.GetPendingLinkedInvites()
5310 self.assertEqual([222], as_parent)
5311 self.assertEqual([555], as_child)
5312
5313 def testInviteLinkedParent_MissingParent(self):
5314 """Invited parent must be specified by email."""
5315 with self.work_env as we:
5316 with self.assertRaises(exceptions.InputException):
5317 we.InviteLinkedParent('')
5318
5319 def testInviteLinkedParent_Anon(self):
5320 """Anon cannot invite anyone to link accounts."""
5321 with self.work_env as we:
5322 with self.assertRaises(permissions.PermissionException):
5323 we.InviteLinkedParent('x@example.com')
5324
5325 def testInviteLinkedParent_NotAMatch(self):
5326 """We only allow linkage invites when usernames match."""
5327 self.SignIn()
5328 with self.work_env as we:
5329 with self.assertRaises(exceptions.InputException) as cm:
5330 we.InviteLinkedParent('x@example.com')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005331 self.assertEqual('Linked account names must match', str(cm.exception))
Copybara854996b2021-09-07 19:36:02 +00005332
5333 @mock.patch('settings.linkable_domains', {'example.com': ['other.com']})
5334 def testInviteLinkedParent_BadDomain(self):
5335 """We only allow linkage invites between allowlisted domains."""
5336 self.SignIn()
5337 with self.work_env as we:
5338 with self.assertRaises(exceptions.InputException) as cm:
5339 we.InviteLinkedParent('user_111@hacker.com')
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005340 self.assertEqual('Linked account unsupported domain', str(cm.exception))
Copybara854996b2021-09-07 19:36:02 +00005341
5342 @mock.patch('settings.linkable_domains', {'example.com': ['other.com']})
5343 def testInviteLinkedParent_NoSuchParent(self):
5344 """Verify that the parent account already exists."""
5345 self.SignIn()
5346 with self.work_env as we:
5347 with self.assertRaises(exceptions.NoSuchUserException):
5348 we.InviteLinkedParent('user_111@other.com')
5349
5350 @mock.patch('settings.linkable_domains', {'example.com': ['other.com']})
5351 def testInviteLinkedParent_Normal(self):
5352 """A child account can invite a matching parent account to link."""
5353 self.services.user.TestAddUser('user_111@other.com', 555)
5354 self.SignIn()
5355 with self.work_env as we:
5356 we.InviteLinkedParent('user_111@other.com')
5357 self.assertEqual(
5358 [(555, 111)], self.services.user.invite_rows)
5359
5360 def testAcceptLinkedChild_NoInvite(self):
5361 """A parent account can only accept an exiting invite."""
5362 self.SignIn()
5363 self.services.user.invite_rows = [(111, 222)]
5364 with self.work_env as we:
5365 with self.assertRaises(exceptions.InputException):
5366 we.AcceptLinkedChild(333)
5367
5368 self.SignIn(user_id=222)
5369 self.services.user.invite_rows = [(111, 333)]
5370 with self.work_env as we:
5371 with self.assertRaises(exceptions.InputException):
5372 we.AcceptLinkedChild(333)
5373
5374 def testAcceptLinkedChild_Normal(self):
5375 """A parent account can accept an invite from a child."""
5376 self.SignIn()
5377 self.services.user.invite_rows = [(111, 222)]
5378 with self.work_env as we:
5379 we.AcceptLinkedChild(222)
5380 self.assertEqual(
5381 [(111, 222)], self.services.user.linked_account_rows)
5382 self.assertEqual(
5383 [], self.services.user.invite_rows)
5384
5385 def testUnlinkAccounts_NotAllowed(self):
5386 """Reject attempts to unlink someone else's accounts."""
5387 self.SignIn(user_id=333)
5388 with self.work_env as we:
5389 with self.assertRaises(permissions.PermissionException):
5390 we.UnlinkAccounts(111, 222)
5391
5392 def testUnlinkAccounts_AdminIsAllowed(self):
5393 """Site admins may unlink someone else's accounts."""
5394 self.SignIn(user_id=444)
5395 self.services.user.linked_account_rows = [(111, 222)]
5396 with self.work_env as we:
5397 we.UnlinkAccounts(111, 222)
5398 self.assertNotIn((111, 222), self.services.user.linked_account_rows)
5399
5400 def testUnlinkAccounts_Normal(self):
5401 """A parent or child can unlink their linked account."""
5402 self.SignIn(user_id=111)
5403 self.services.user.linked_account_rows = [(111, 222), (333, 444)]
5404 with self.work_env as we:
5405 we.UnlinkAccounts(111, 222)
5406 self.assertEqual([(333, 444)], self.services.user.linked_account_rows)
5407
5408 self.SignIn(user_id=222)
5409 self.services.user.linked_account_rows = [(111, 222), (333, 444)]
5410 with self.work_env as we:
5411 we.UnlinkAccounts(111, 222)
5412 self.assertEqual([(333, 444)], self.services.user.linked_account_rows)
5413
5414 def testUpdateUserSettings(self):
5415 """We can update the settings of the logged in user."""
5416 self.SignIn()
5417 user = self.services.user.GetUser(self.cnxn, 111)
5418 with self.work_env as we:
5419 we.UpdateUserSettings(
5420 user,
5421 obscure_email=True,
5422 keep_people_perms_open=True)
5423
5424 self.assertTrue(user.obscure_email)
5425 self.assertTrue(user.keep_people_perms_open)
5426
5427 def testUpdateUserSettings_Anon(self):
5428 """A user must be logged in."""
5429 anon = self.services.user.GetUser(self.cnxn, 0)
5430 with self.work_env as we:
5431 with self.assertRaises(exceptions.InputException):
5432 we.UpdateUserSettings(anon, keep_people_perms_open=True)
5433
5434 def testGetUserPrefs_Anon(self):
5435 """Anon always has empty prefs."""
5436 with self.work_env as we:
5437 userprefs = we.GetUserPrefs(0)
5438
5439 self.assertEqual(0, userprefs.user_id)
5440 self.assertEqual([], userprefs.prefs)
5441
5442 def testGetUserPrefs_Mine_Empty(self):
5443 """User who never set any pref gets empty prefs."""
5444 self.SignIn()
5445 with self.work_env as we:
5446 userprefs = we.GetUserPrefs(111)
5447
5448 self.assertEqual(111, userprefs.user_id)
5449 self.assertEqual([], userprefs.prefs)
5450
5451 def testGetUserPrefs_Mine_Some(self):
5452 """User who set a pref gets it back."""
5453 self.services.user.SetUserPrefs(
5454 self.cnxn, 111,
5455 [user_pb2.UserPrefValue(name='code_font', value='true')])
5456 self.SignIn()
5457 with self.work_env as we:
5458 userprefs = we.GetUserPrefs(111)
5459
5460 self.assertEqual(111, userprefs.user_id)
5461 self.assertEqual(1, len(userprefs.prefs))
5462 self.assertEqual('code_font', userprefs.prefs[0].name)
5463 self.assertEqual('true', userprefs.prefs[0].value)
5464
5465 def testGetUserPrefs_Other_Allowed(self):
5466 """A site admin can read another user's prefs."""
5467 self.services.user.SetUserPrefs(
5468 self.cnxn, 111,
5469 [user_pb2.UserPrefValue(name='code_font', value='true')])
5470 self.SignIn(user_id=self.admin_user.user_id)
5471
5472 with self.work_env as we:
5473 userprefs = we.GetUserPrefs(111)
5474
5475 self.assertEqual(111, userprefs.user_id)
5476 self.assertEqual(1, len(userprefs.prefs))
5477 self.assertEqual('code_font', userprefs.prefs[0].name)
5478 self.assertEqual('true', userprefs.prefs[0].value)
5479
5480 def testGetUserPrefs_Other_Denied(self):
5481 """A non-admin cannot read another user's prefs."""
5482 self.services.user.SetUserPrefs(
5483 self.cnxn, 111,
5484 [user_pb2.UserPrefValue(name='code_font', value='true')])
5485 # user2 is not a site admin.
5486 self.SignIn(222)
5487
5488 with self.work_env as we:
5489 with self.assertRaises(permissions.PermissionException):
5490 we.GetUserPrefs(111)
5491
5492 def _SetUpCorpUsers(self, user_ids):
5493 self.services.user.TestAddUser('corp_group@example.com', 888)
5494 self.services.usergroup.TestAddGroupSettings(
5495 888, 'corp_group@example.com')
5496 self.services.usergroup.TestAddMembers(888, user_ids)
5497
5498 # TODO(jrobbins): Update this with user group prefs when implemented.
5499 @mock.patch(
5500 'settings.restrict_new_issues_user_groups', ['corp_group@example.com'])
5501 def testGetUserPrefs_Mine_RestrictNewIssues(self):
5502 """User who belongs to restrict_new_issues user group gets those prefs."""
5503 self._SetUpCorpUsers([111, 222])
5504 self.services.user.SetUserPrefs(
5505 self.cnxn, 111,
5506 [user_pb2.UserPrefValue(name='code_font', value='true')])
5507 self.SignIn()
5508 with self.work_env as we:
5509 userprefs = we.GetUserPrefs(111)
5510
5511 self.assertEqual(111, userprefs.user_id)
5512 self.assertEqual(2, len(userprefs.prefs))
5513 self.assertEqual('code_font', userprefs.prefs[0].name)
5514 self.assertEqual('true', userprefs.prefs[0].value)
5515 self.assertEqual('restrict_new_issues', userprefs.prefs[1].name)
5516 self.assertEqual('true', userprefs.prefs[1].value)
5517
5518 @mock.patch(
5519 'settings.restrict_new_issues_user_groups', ['corp_group@example.com'])
5520 def testGetUserPrefs_Mine_RestrictNewIssues_OptedOut(self):
5521 """If a restrict_new_issues user has opted out, use that pref value."""
5522 self._SetUpCorpUsers([111, 222])
5523 self.services.user.SetUserPrefs(
5524 self.cnxn, 111,
5525 [user_pb2.UserPrefValue(name='restrict_new_issues', value='false')])
5526 self.SignIn()
5527 with self.work_env as we:
5528 userprefs = we.GetUserPrefs(111)
5529
5530 self.assertEqual(111, userprefs.user_id)
5531 self.assertEqual(1, len(userprefs.prefs))
5532 self.assertEqual('restrict_new_issues', userprefs.prefs[0].name)
5533 self.assertEqual('false', userprefs.prefs[0].value)
5534
5535 # TODO(jrobbins): Update this with user group prefs when implemented.
5536 @mock.patch(
5537 'settings.public_issue_notice_user_groups', ['corp_group@example.com'])
5538 def testGetUserPrefs_Mine_PublicIssueNotice(self):
5539 """User who belongs to public_issue_notice user group gets those prefs."""
5540 self._SetUpCorpUsers([111, 222])
5541 self.services.user.SetUserPrefs(
5542 self.cnxn, 111,
5543 [user_pb2.UserPrefValue(name='code_font', value='true')])
5544 self.SignIn()
5545 with self.work_env as we:
5546 userprefs = we.GetUserPrefs(111)
5547
5548 self.assertEqual(111, userprefs.user_id)
5549 self.assertEqual(2, len(userprefs.prefs))
5550 self.assertEqual('code_font', userprefs.prefs[0].name)
5551 self.assertEqual('true', userprefs.prefs[0].value)
5552 self.assertEqual('public_issue_notice', userprefs.prefs[1].name)
5553 self.assertEqual('true', userprefs.prefs[1].value)
5554
5555 @mock.patch(
5556 'settings.public_issue_notice_user_groups', ['corp_group@example.com'])
5557 def testGetUserPrefs_Mine_PublicIssueNotice_OptedOut(self):
5558 """If a public_issue_notice user has opted out, use that pref value."""
5559 self._SetUpCorpUsers([111, 222])
5560 self.services.user.SetUserPrefs(
5561 self.cnxn, 111,
5562 [user_pb2.UserPrefValue(name='public_issue_notice', value='false')])
5563 self.SignIn()
5564 with self.work_env as we:
5565 userprefs = we.GetUserPrefs(111)
5566
5567 self.assertEqual(111, userprefs.user_id)
5568 self.assertEqual(1, len(userprefs.prefs))
5569 self.assertEqual('public_issue_notice', userprefs.prefs[0].name)
5570 self.assertEqual('false', userprefs.prefs[0].value)
5571
5572 def testSetUserPrefs_Anon(self):
5573 """Anon cannot set prefs."""
5574 with self.work_env as we:
5575 with self.assertRaises(exceptions.InputException):
5576 we.SetUserPrefs(0, [])
5577
5578 def testSetUserPrefs_Mine_Empty(self):
5579 """Setting zero prefs is a no-op.."""
5580 self.SignIn(111)
5581
5582 with self.work_env as we:
5583 we.SetUserPrefs(111, [])
5584
5585 prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
5586 self.assertEqual(0, len(prefs_after.prefs))
5587
5588 def testSetUserPrefs_Mine_Add(self):
5589 """User can set a preference for the first time."""
5590 self.SignIn(111)
5591
5592 with self.work_env as we:
5593 we.SetUserPrefs(
5594 111,
5595 [user_pb2.UserPrefValue(name='code_font', value='true')])
5596
5597 prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
5598 self.assertEqual(1, len(prefs_after.prefs))
5599 self.assertEqual('code_font', prefs_after.prefs[0].name)
5600 self.assertEqual('true', prefs_after.prefs[0].value)
5601
5602 def testSetUserPrefs_Mine_Overwrite(self):
5603 """User can change the value of a pref."""
5604 self.SignIn(111)
5605 self.services.user.SetUserPrefs(
5606 self.cnxn, 111,
5607 [user_pb2.UserPrefValue(name='code_font', value='true')])
5608
5609 with self.work_env as we:
5610 we.SetUserPrefs(
5611 111,
5612 [user_pb2.UserPrefValue(name='code_font', value='false')])
5613
5614 prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
5615 self.assertEqual(1, len(prefs_after.prefs))
5616 self.assertEqual('code_font', prefs_after.prefs[0].name)
5617 self.assertEqual('false', prefs_after.prefs[0].value)
5618
5619 def testSetUserPrefs_Mine_Bad(self):
5620 """User cannot set a preference value that is not valid."""
5621 self.SignIn(111)
5622
5623 with self.work_env as we:
5624 with self.assertRaises(exceptions.InputException):
5625 we.SetUserPrefs(
5626 111,
5627 [user_pb2.UserPrefValue(name='code_font', value='sorta')])
5628 with self.assertRaises(exceptions.InputException):
5629 we.SetUserPrefs(
5630 111,
5631 [user_pb2.UserPrefValue(name='sign', value='gemini')])
5632
5633 # Regardless of exceptions, nothing was actually stored.
5634 prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
5635 self.assertEqual(0, len(prefs_after.prefs))
5636
5637 def testSetUserPrefs_Other_Allowed(self):
5638 """A site admin can update another user's prefs."""
5639 self.SignIn(user_id=self.admin_user.user_id)
5640 self.services.user.SetUserPrefs(
5641 self.cnxn, 111,
5642 [user_pb2.UserPrefValue(name='code_font', value='true')])
5643
5644 with self.work_env as we:
5645 we.SetUserPrefs(
5646 111,
5647 [user_pb2.UserPrefValue(name='code_font', value='false')])
5648
5649 prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
5650 self.assertEqual(1, len(prefs_after.prefs))
5651 self.assertEqual('code_font', prefs_after.prefs[0].name)
5652 self.assertEqual('false', prefs_after.prefs[0].value)
5653
5654 def testSetUserPrefs_Other_Denied(self):
5655 """A non-admin cannot set another user's prefs."""
5656 # user2 is not a site admin.
5657 self.SignIn(222)
5658 self.services.user.SetUserPrefs(
5659 self.cnxn, 111,
5660 [user_pb2.UserPrefValue(name='code_font', value='true')])
5661
5662 with self.work_env as we:
5663 with self.assertRaises(permissions.PermissionException):
5664 we.SetUserPrefs(
5665 111,
5666 [user_pb2.UserPrefValue(name='code_font', value='false')])
5667
5668 # Regardless of any exception, the preferences remain unchanged.
5669 prefs_after = self.services.user.GetUserPrefs(self.cnxn, 111)
5670 self.assertEqual(1, len(prefs_after.prefs))
5671 self.assertEqual('code_font', prefs_after.prefs[0].name)
5672 self.assertEqual('true', prefs_after.prefs[0].value)
5673
5674 # FUTURE: GetUser()
5675 # FUTURE: UpdateUser()
5676 # FUTURE: DeleteUser()
5677 # FUTURE: ListStarredUsers()
5678
5679 def testExpungeUsers_PermissionException(self):
5680 with self.assertRaises(permissions.PermissionException):
5681 with self.work_env as we:
5682 we.ExpungeUsers([])
5683
5684 def testExpungeUsers_NoUsers(self):
5685 self.mr.cnxn = mock.Mock()
5686 self.mr.cnxn.Commit = mock.Mock()
5687 self.services.usergroup.group_dag = mock.Mock()
5688
5689 self.mr.perms = permissions.ADMIN_PERMISSIONSET
5690 with self.work_env as we:
5691 we.ExpungeUsers(['unknown@user.test'])
5692
5693 self.mr.cnxn.Commit.assert_not_called()
5694 self.services.usergroup.group_dag.MarkObsolete.assert_not_called()
5695
5696 def testExpungeUsers_ReservedUserID(self):
5697 self.mr.cnxn = mock.Mock()
5698 self.mr.cnxn.Commit = mock.Mock()
5699 self.services.usergroup.group_dag = mock.Mock()
5700
5701 user_1 = self.services.user.TestAddUser(
5702 'tainted-data@user.test', framework_constants.DELETED_USER_ID)
5703
5704 self.mr.perms = permissions.ADMIN_PERMISSIONSET
5705 with self.assertRaises(exceptions.InputException):
5706 with self.work_env as we:
5707 we.ExpungeUsers([user_1.email])
5708
5709 @mock.patch(
5710 'features.send_notifications.'
5711 'PrepareAndSendDeletedFilterRulesNotification')
5712 def testExpungeUsers_SkipPermissieons(self, _fake_pasdfrn):
5713 self.mr.cnxn = mock.Mock()
5714 self.services.usergroup.group_dag = mock.Mock()
5715 with self.work_env as we:
5716 we.ExpungeUsers([], check_perms=False)
5717
5718 @mock.patch(
5719 'features.send_notifications.'
5720 'PrepareAndSendDeletedFilterRulesNotification')
5721 def testExpungeUsers(self, fake_pasdfrn):
5722 """Test user data correctly expunged."""
5723 # Replace template service mock with fake testing TemplateService
5724 self.services.template = fake.TemplateService()
5725
5726 wipeout_emails = ['cow@test.com', 'chicken@test.com', 'llama@test.com',
5727 'alpaca@test.com']
5728 user_1 = self.services.user.TestAddUser('cow@test.com', 111)
5729 user_2 = self.services.user.TestAddUser('chicken@test.com', 222)
5730 user_3 = self.services.user.TestAddUser('llama@test.com', 333)
5731 user_4 = self.services.user.TestAddUser('random@test.com', 888)
5732 ids_by_email = {user_1.email: user_1.user_id, user_2.email: user_2.user_id,
5733 user_3.email: user_3.user_id}
5734 user_ids = list(ids_by_email.values())
5735
5736 # set up testing data
5737 starred_project_id = 19
5738 self.services.project_star._SetStar(self.mr.cnxn, 12, user_1.user_id, True)
5739 self.services.user_star.SetStar(
5740 self.mr.cnxn, user_2.user_id, user_4.user_id, True)
5741 template = self.services.template.TestAddIssueTemplateDef(
5742 13, 16, 'template name', owner_id=user_3.user_id)
5743 project1 = self.services.project.TestAddProject(
5744 'project1', owner_ids=[111, 333], project_id=16)
5745 project2 = self.services.project.TestAddProject(
5746 'project2',owner_ids=[888], contrib_ids=[111, 222],
5747 committer_ids=[333], project_id=17)
5748
5749 self.services.features.TestAddFilterRule(
5750 16, 'owner:cow@test.com', add_cc_ids=[user_4.user_id])
5751 self.services.features.TestAddFilterRule(
5752 16, 'owner:random@test.com',
5753 add_cc_ids=[user_2.user_id, user_3.user_id])
5754 self.services.features.TestAddFilterRule(
5755 17, 'label:random-label', add_notify=[user_3.email])
5756 kept_rule = self.services.features.TestAddFilterRule(
5757 16, 'owner:random@test.com', add_notify=['random2@test.com'])
5758
5759 self.mr.cnxn = mock.Mock()
5760 self.services.usergroup.group_dag = mock.Mock()
5761
5762 # call ExpungeUsers
5763 self.mr.perms = permissions.ADMIN_PERMISSIONSET
5764 with self.work_env as we:
5765 we.ExpungeUsers(wipeout_emails)
5766
5767 # Assert users expunged in stars
5768 self.assertFalse(self.services.project_star.IsItemStarredBy(
5769 self.mr.cnxn, starred_project_id, user_1.user_id))
5770 self.assertFalse(self.services.user_star.CountItemStars(
5771 self.mr.cnxn, user_2.user_id))
5772
5773 # Assert users expunged in quick edits and saved queries
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005774 six.assertCountEqual(
5775 self, self.services.features.expunged_users_in_quick_edits, user_ids)
5776 six.assertCountEqual(
5777 self, self.services.features.expunged_users_in_saved_queries, user_ids)
Copybara854996b2021-09-07 19:36:02 +00005778
5779 # Assert users expunged in templates and configs
5780 self.assertIsNone(template.owner_id)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005781 six.assertCountEqual(
5782 self, self.services.config.expunged_users_in_configs, user_ids)
Copybara854996b2021-09-07 19:36:02 +00005783
5784 # Assert users expunged in projects
5785 self.assertEqual(project1.owner_ids, [])
5786 self.assertEqual(project2.contributor_ids, [])
5787
5788 # Assert users expunged in issues
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005789 six.assertCountEqual(
5790 self, self.services.issue.expunged_users_in_issues, user_ids)
Copybara854996b2021-09-07 19:36:02 +00005791 self.assertTrue(self.services.issue.enqueue_issues_called)
5792
5793 # Assert users expunged in spam
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005794 six.assertCountEqual(
5795 self, self.services.spam.expunged_users_in_spam, user_ids)
Copybara854996b2021-09-07 19:36:02 +00005796
5797 # Assert users expunged in hotlists
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005798 six.assertCountEqual(
5799 self, self.services.features.expunged_users_in_hotlists, user_ids)
Copybara854996b2021-09-07 19:36:02 +00005800
5801 # Assert users expunged in groups
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005802 six.assertCountEqual(
5803 self, self.services.usergroup.expunged_users_in_groups, user_ids)
Copybara854996b2021-09-07 19:36:02 +00005804
5805 # Assert filter rules expunged
5806 self.assertEqual(
5807 self.services.features.test_rules[16], [kept_rule])
5808 self.assertEqual(
5809 self.services.features.test_rules[17], [])
5810
5811 # Assert mocks
5812 self.assertEqual(7, len(self.mr.cnxn.Commit.call_args_list))
5813 self.services.usergroup.group_dag.MarkObsolete.assert_called_once()
5814
5815 fake_pasdfrn.assert_has_calls(
5816 [mock.call(
5817 16,
5818 'testing-app.appspot.com',
5819 ['if owner:%s then add cc(s): random@test.com' % (
5820 framework_constants.DELETED_USER_NAME),
5821 'if owner:random@test.com then add cc(s): %s, %s' % (
5822 framework_constants.DELETED_USER_NAME,
5823 framework_constants.DELETED_USER_NAME)]),
5824 mock.call(
5825 17,
5826 'testing-app.appspot.com',
5827 ['if label:random-label then notify: %s' % (
5828 framework_constants.DELETED_USER_NAME)])
5829 ])
5830
5831 def testTotalUsersCount_WithDeletedUser(self):
5832 # Clear users added previously with TestAddUser
5833 self.services.user.users_by_id = {}
5834 self.services.user.TestAddUser(
5835 '', framework_constants.DELETED_USER_ID)
5836 self.services.user.TestAddUser('cow@test.com', 111)
5837 self.services.user.TestAddUser('chicken@test.com', 222)
5838 self.assertEqual(2, self.services.user.TotalUsersCount(self.mr.cnxn))
5839
5840 def testTotalUsersCount(self):
5841 # Clear users added previously with TestAddUser
5842 self.services.user.users_by_id = {}
5843 self.services.user.TestAddUser('cow@test.com', 111)
5844 self.assertEqual(1, self.services.user.TotalUsersCount(self.mr.cnxn))
5845
5846 def testGetAllUserEmailsBatch(self):
5847 # Clear users added previously with TestAddUser
5848 self.services.user.users_by_id = {}
5849 user_1 = self.services.user.TestAddUser('cow@test.com', 111)
5850 user_2 = self.services.user.TestAddUser('chicken@test.com', 222)
5851 user_6 = self.services.user.TestAddUser('6@test.com', 666)
5852 user_5 = self.services.user.TestAddUser('5@test.com', 555)
5853 user_3 = self.services.user.TestAddUser('3@test.com', 333)
5854 self.services.user.TestAddUser('4@test.com', 444)
5855
5856
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005857 six.assertCountEqual(
5858 self, [user_1.email, user_2.email, user_3.email],
Copybara854996b2021-09-07 19:36:02 +00005859 self.services.user.GetAllUserEmailsBatch(self.mr.cnxn, limit=3))
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005860 six.assertCountEqual(
5861 self, [user_5.email, user_6.email],
Copybara854996b2021-09-07 19:36:02 +00005862 self.services.user.GetAllUserEmailsBatch(
5863 self.mr.cnxn, limit=3, offset=4))
5864
5865 # Test existence of deleted user does not change results.
5866 self.services.user.TestAddUser(
5867 '', framework_constants.DELETED_USER_ID)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005868 six.assertCountEqual(
5869 self, [user_1.email, user_2.email, user_3.email],
Copybara854996b2021-09-07 19:36:02 +00005870 self.services.user.GetAllUserEmailsBatch(self.mr.cnxn, limit=3))
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005871 six.assertCountEqual(
5872 self, [user_5.email, user_6.email],
Copybara854996b2021-09-07 19:36:02 +00005873 self.services.user.GetAllUserEmailsBatch(
5874 self.mr.cnxn, limit=3, offset=4))
5875
5876 # FUTURE: CreateGroup()
5877 # FUTURE: ListGroups()
5878 # FUTURE: UpdateGroup()
5879 # FUTURE: DeleteGroup()
5880
5881 def AddIssueToHotlist(self, hotlist_id, issue_id=78901, adder_id=111):
5882 self.services.features.AddIssuesToHotlists(
5883 self.cnxn, [hotlist_id], [(issue_id, adder_id, 0, '')],
5884 None, None, None)
5885
5886 def testCreateHotlist_Normal(self):
5887 """We can create a hotlist."""
5888 issue_1 = fake.MakeTestIssue(
5889 789, 1, 'sum', 'New', 111, issue_id=78901)
5890 self.services.issue.TestAddIssue(issue_1)
5891
5892 self.SignIn()
5893 with self.work_env as we:
5894 hotlist = we.CreateHotlist(
5895 'name', 'summary', 'description', [222], [78901], False,
5896 'priority owner')
5897
5898 self.assertEqual('name', hotlist.name)
5899 self.assertEqual('summary', hotlist.summary)
5900 self.assertEqual('description', hotlist.description)
5901 self.assertEqual([111], hotlist.owner_ids)
5902 self.assertEqual([222], hotlist.editor_ids)
5903 self.assertEqual([78901], [item.issue_id for item in hotlist.items])
5904 self.assertEqual(False, hotlist.is_private)
5905 self.assertEqual('priority owner', hotlist.default_col_spec)
5906
5907 def testCreateHotlist_NotViewable(self):
5908 """We cannot add issues we cannot see to a hotlist."""
5909 hotlist_owner_id = 333
5910 issue1 = fake.MakeTestIssue(
5911 789, 1, 'sum1', 'New', 111, issue_id=78901,
5912 labels=['Restrict-View-Chicken'])
5913 self.services.issue.TestAddIssue(issue1)
5914
5915 self.SignIn(user_id=hotlist_owner_id)
5916 with self.assertRaises(permissions.PermissionException):
5917 with self.work_env as we:
5918 we.CreateHotlist(
5919 'Cow-Hotlist', 'Moo', 'MooMoo', [], [issue1.issue_id], False, '')
5920
5921 def testCreateHotlist_AnonCantCreateHotlist(self):
5922 """We must be signed in to create a hotlist."""
5923 with self.assertRaises(exceptions.InputException):
5924 with self.work_env as we:
5925 we.CreateHotlist('name', 'summary', 'description', [], [222], False, '')
5926
5927 def testCreateHotlist_InvalidName(self):
5928 """We can't create a hotlist with an invalid name."""
5929 self.SignIn()
5930 with self.assertRaises(exceptions.InputException):
5931 with self.work_env as we:
5932 we.CreateHotlist(
5933 '***Invalid***', 'summary', 'description', [], [], False, '')
5934
5935 def testCreateHotlist_HotlistAlreadyExists(self):
5936 """We can't create a hotlist with a name that already exists."""
5937 self.SignIn()
5938 with self.work_env as we:
5939 we.CreateHotlist('name', 'summary', 'description', [], [], False, '')
5940
5941 with self.assertRaises(features_svc.HotlistAlreadyExists):
5942 with self.work_env as we:
5943 we.CreateHotlist('name', 'foo', 'bar', [], [], True, '')
5944
5945 def testUpdateHotlist(self):
5946 """We can update a hotlist."""
5947 self.SignIn(user_id=self.user_1.user_id)
5948 with self.work_env as we:
5949 we.UpdateHotlist(
5950 self.hotlist.hotlist_id, hotlist_name=self.hotlist.name,
5951 summary='new sum', description='new desc',
5952 owner_id=self.user_2.user_id,
5953 add_editor_ids=[self.user_1.user_id, self.user_3.user_id],
5954 is_private=False)
5955 updated_hotlist = we.GetHotlist(self.hotlist.hotlist_id)
5956
5957 expected_hotlist = features_pb2.Hotlist(
5958 hotlist_id=self.hotlist.hotlist_id, name=self.hotlist.name,
5959 summary='new sum', description='new desc',
5960 owner_ids=[self.user_2.user_id],
5961 editor_ids=[self.user_2.user_id,
5962 self.user_3.user_id,
5963 self.user_1.user_id],
5964 is_private=False)
5965 self.assertEqual(updated_hotlist, expected_hotlist)
5966
5967 @mock.patch('testing.fake.FeaturesService.UpdateHotlist')
5968 def testUpdateHotlist_NoChanges(self, fake_update_hotlist):
5969 """The DB does not get updated if all changes are no-op changes"""
5970 self.SignIn(user_id=self.user_1.user_id)
5971 with self.work_env as we:
5972 we.UpdateHotlist(
5973 self.hotlist.hotlist_id, hotlist_name=self.hotlist.name,
5974 owner_id=self.user_1.user_id,
5975 add_editor_ids=[self.user_1.user_id, self.user_2.user_id],
5976 is_private=self.hotlist.is_private,
5977 default_col_spec=self.hotlist.default_col_spec,
5978 summary=self.hotlist.summary,
5979 description=self.hotlist.description)
5980 updated_hotlist = we.GetHotlist(self.hotlist.hotlist_id)
5981
5982 self.assertEqual(updated_hotlist, self.hotlist)
5983 fake_update_hotlist.assert_not_called()
5984
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01005985 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +00005986 def testUpdateHotlist_HotlistNotFound(self):
5987 """Error is thrown when a hotlist is not found."""
5988 self.SignIn(user_id=self.user_1.user_id)
5989 with self.assertRaises(features_svc.NoSuchHotlistException):
5990 with self.work_env as we:
5991 we.UpdateHotlist(404)
5992
5993 def testUpdateHotlist_NoPermissions(self):
5994 """Error is thrown when the user doesn't have administer permisisons."""
5995 self.SignIn(user_id=self.user_2.user_id)
5996 with self.assertRaises(permissions.PermissionException):
5997 with self.work_env as we:
5998 we.UpdateHotlist(self.hotlist.hotlist_id)
5999
6000 def testUpdateHotlist_InvalidName(self):
6001 """Error is thrown when proposed new name is invalid."""
6002 self.SignIn(user_id=self.user_1.user_id)
6003 with self.assertRaises(exceptions.InputException):
6004 with self.work_env as we:
6005 we.UpdateHotlist(self.hotlist.hotlist_id, hotlist_name='-Chicken')
6006
6007 def testUpdateHotlist_HotlistAlreadyExistsOwnerChange(self):
6008 """Error is thrown proposed owner has hotlist with same name."""
6009 _hotlist_conflict = self.work_env.services.features.TestAddHotlist(
6010 'myhotlist', summary='old sum', owner_ids=[self.user_2.user_id],
6011 description='old desc', hotlist_id=458, is_private=True)
6012 self.SignIn(user_id=self.user_1.user_id)
6013 with self.assertRaises(features_svc.HotlistAlreadyExists):
6014 with self.work_env as we:
6015 we.UpdateHotlist(self.hotlist.hotlist_id, owner_id=self.user_2.user_id)
6016
6017 def testUpdateHotlist_HotlistAlreadyExistsNameChange(self):
6018 """Error is thrown when owner already has a hotlist with same name as
6019 proposed name."""
6020 hotlist_conflict = self.work_env.services.features.TestAddHotlist(
6021 'myhotlist2', summary='old sum', owner_ids=[self.user_1.user_id],
6022 description='old desc', hotlist_id=458, is_private=True)
6023 self.SignIn(user_id=self.user_1.user_id)
6024 with self.assertRaises(features_svc.HotlistAlreadyExists):
6025 with self.work_env as we:
6026 we.UpdateHotlist(
6027 self.hotlist.hotlist_id, hotlist_name=hotlist_conflict.name)
6028
6029 def testUpdateHotlist_HotlistAlreadyExistsNameAndOwnerChange(self):
6030 """Error is thrown when new owner already has hotlist with same new name."""
6031 hotlist_conflict = self.work_env.services.features.TestAddHotlist(
6032 'myhotlist2', summary='old sum', owner_ids=[self.user_2.user_id],
6033 description='old desc', hotlist_id=458, is_private=True)
6034 self.SignIn(user_id=self.user_1.user_id)
6035 with self.assertRaises(features_svc.HotlistAlreadyExists):
6036 with self.work_env as we:
6037 we.UpdateHotlist(
6038 self.hotlist.hotlist_id, owner_id=self.user_2.user_id,
6039 hotlist_name=hotlist_conflict.name)
6040
6041 def testGetHotlist_Normal(self):
6042 """We can get an existing hotlist by hotlist_id."""
6043 hotlist = self.work_env.services.features.CreateHotlist(
6044 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6045 owner_ids=[111], editor_ids=[])
6046
6047 with self.work_env as we:
6048 actual = we.GetHotlist(hotlist.hotlist_id)
6049
6050 self.assertEqual(hotlist, actual)
6051
6052 def testGetHotlist_NoneHotlist(self):
6053 """We reject attempts to pass a None hotlist_id."""
6054 with self.assertRaises(exceptions.InputException):
6055 with self.work_env as we:
6056 _actual = we.GetHotlist(None)
6057
6058 def testGetHotlist_NoSuchHotlist(self):
6059 """We reject attempts to get a non-existent hotlist."""
6060 with self.assertRaises(features_svc.NoSuchHotlistException):
6061 with self.work_env as we:
6062 _actual = we.GetHotlist(999)
6063
6064 def testListHotlistItems_MoreItems(self):
6065 """We can get hotlist's sorted HotlistItems and next start index."""
6066 owner_ids = [self.user_1.user_id]
6067 issue1 = fake.MakeTestIssue(
6068 789, 1, 'sum', 'New', self.user_1.user_id, issue_id=78901)
6069 self.services.issue.TestAddIssue(issue1)
6070 self.services.project.TestAddProject(
6071 'proj', project_id=788, committer_ids=[self.user_1.user_id])
6072 issue2 = fake.MakeTestIssue(
6073 788, 2, 'sum', 'New', self.user_1.user_id, issue_id=78802)
6074 self.services.issue.TestAddIssue(issue2)
6075 issue3 = fake.MakeTestIssue(
6076 789, 3, 'sum', 'New', self.user_3.user_id, issue_id=78803)
6077 self.services.issue.TestAddIssue(issue3)
6078 base_date = 1205079300
6079 hotlist_item_tuples = [
6080 (issue1.issue_id, 1, self.user_1.user_id, base_date + 2, 'dude wheres'),
6081 (issue2.issue_id, 31, self.user_1.user_id, base_date + 1, 'my car'),
6082 (issue3.issue_id, 21, self.user_1.user_id, base_date, '')]
6083 hotlist = self.work_env.services.features.TestAddHotlist(
6084 'hotlist', summary='Summary', description='Description',
6085 owner_ids=owner_ids, hotlist_id=123,
6086 hotlist_item_fields=hotlist_item_tuples)
6087
6088 self.SignIn(user_id=self.user_1.user_id)
6089 with self.work_env as we:
6090 max_items = 2
6091 start = 0
6092 can = 1
6093 sort_spec = 'rank'
6094 group_by_spec = ''
6095 list_result = we.ListHotlistItems(
6096 hotlist.hotlist_id, max_items, start, can, sort_spec, group_by_spec)
6097
6098 expected_items = [
6099 features_pb2.Hotlist.HotlistItem(
6100 issue_id=issue1.issue_id, rank=1, adder_id=self.user_1.user_id,
6101 date_added=base_date + 2, note='dude wheres'),
6102 features_pb2.Hotlist.HotlistItem(
6103 issue_id=issue3.issue_id, rank=21, adder_id=self.user_1.user_id,
6104 date_added=base_date, note='')]
6105 self.assertEqual(list_result.items, expected_items)
6106
6107 self.assertEqual(list_result.next_start, 2)
6108
6109 def testListHotlistItems_OutOfRange(self):
6110 """We can handle out of range `start` and `max_items`."""
6111 owner_ids = [self.user_1.user_id]
6112 issue1 = fake.MakeTestIssue(
6113 789, 1, 'sum', 'New', self.user_1.user_id, issue_id=78901)
6114 self.services.issue.TestAddIssue(issue1)
6115 self.services.project.TestAddProject(
6116 'proj', project_id=788, committer_ids=[self.user_1.user_id])
6117 base_date = 1205079300
6118 hotlist_item_tuples = [
6119 (issue1.issue_id, 1, self.user_1.user_id, base_date + 2, 'dude wheres')]
6120 hotlist = self.work_env.services.features.TestAddHotlist(
6121 'hotlist', summary='Summary', description='Description',
6122 owner_ids=owner_ids, hotlist_id=123,
6123 hotlist_item_fields=hotlist_item_tuples)
6124
6125 self.SignIn(user_id=self.user_1.user_id)
6126 with self.work_env as we:
6127 max_items = 10
6128 start = 4
6129 can = 1
6130 sort_spec = ''
6131 group_by_spec = ''
6132 list_result = we.ListHotlistItems(
6133 hotlist.hotlist_id, max_items, start, can, sort_spec, group_by_spec)
6134
6135 self.assertEqual(list_result.items, [])
6136
6137 self.assertIsNone(list_result.next_start)
6138
6139 def testListHotlistItems_InvalidMaxItems(self):
6140 """We raise an exception if the given max_items is invalid."""
6141 owner_ids = [self.user_1.user_id]
6142 hotlist = self.work_env.services.features.TestAddHotlist(
6143 'hotlist',
6144 summary='Summary',
6145 description='Description',
6146 owner_ids=owner_ids,
6147 hotlist_id=123)
6148
6149 self.SignIn(user_id=self.user_1.user_id)
6150 with self.assertRaises(exceptions.InputException):
6151 with self.work_env as we:
6152 max_items = -2
6153 start = 0
6154 can = 1
6155 sort_spec = 'rank'
6156 group_by_spec = ''
6157 we.ListHotlistItems(
6158 hotlist.hotlist_id, max_items, start, can, sort_spec, group_by_spec)
6159
6160 def testListHotlistItems_InvalidStart(self):
6161 """We raise an exception if the given start is invalid."""
6162 owner_ids = [self.user_1.user_id]
6163 hotlist = self.work_env.services.features.TestAddHotlist(
6164 'hotlist',
6165 summary='Summary',
6166 description='Description',
6167 owner_ids=owner_ids,
6168 hotlist_id=123)
6169
6170 self.SignIn(user_id=self.user_1.user_id)
6171 with self.assertRaises(exceptions.InputException):
6172 with self.work_env as we:
6173 max_items = 10
6174 start = -1
6175 can = 1
6176 sort_spec = 'rank'
6177 group_by_spec = ''
6178 we.ListHotlistItems(
6179 hotlist.hotlist_id, max_items, start, can, sort_spec, group_by_spec)
6180
6181
6182 def testListHotlistItems_OpenOnly(self):
6183 """We can get hotlist's sorted HotlistItems."""
6184 base_date = 1205079300
6185 owner_ids = [self.user_1.user_id]
6186 issue1 = fake.MakeTestIssue(
6187 789, 1, 'sum', 'New', self.user_1.user_id, issue_id=78901)
6188 self.services.issue.TestAddIssue(issue1)
6189 issue2 = fake.MakeTestIssue(
6190 789, 2, 'sum', 'Fixed', self.user_1.user_id, issue_id=78902,
6191 closed_timestamp=base_date + 10)
6192 self.services.issue.TestAddIssue(issue2)
6193 hotlist_item_tuples = [
6194 (issue1.issue_id, 1, self.user_1.user_id, base_date + 2, 'dude wheres'),
6195 (issue2.issue_id, 31, self.user_1.user_id, base_date + 1, 'my car')]
6196 hotlist = self.work_env.services.features.TestAddHotlist(
6197 'hotlist', summary='Summary', description='Description',
6198 owner_ids=owner_ids, hotlist_id=123,
6199 hotlist_item_fields=hotlist_item_tuples)
6200
6201 self.SignIn(user_id=self.user_1.user_id)
6202 with self.work_env as we:
6203 max_items = 2
6204 start = 0
6205 can = 2
6206 sort_spec = 'rank'
6207 group_by_spec = ''
6208 list_result = we.ListHotlistItems(
6209 hotlist.hotlist_id, max_items, start, can, sort_spec, group_by_spec)
6210
6211 expected_items = [
6212 features_pb2.Hotlist.HotlistItem(
6213 issue_id=issue1.issue_id, rank=1, adder_id=self.user_1.user_id,
6214 date_added=base_date + 2, note='dude wheres')]
6215 self.assertEqual(list_result.items, expected_items)
6216
6217 self.assertIsNone(list_result.next_start)
6218
6219 def testListHotlistItems_HideRestricted(self):
6220 """We can get hotlist's sorted HotlistItems."""
6221 base_date = 1205079300
6222 owner_ids = [self.user_1.user_id]
6223 issue1 = fake.MakeTestIssue(
6224 789, 1, 'sum', 'New', self.user_1.user_id, issue_id=78901)
6225 self.services.issue.TestAddIssue(issue1)
6226 self.services.project.TestAddProject(
6227 'proj', project_id=788, committer_ids=[self.user_1.user_id])
6228 issue2 = fake.MakeTestIssue(
6229 788, 2, 'sum', 'New', self.user_1.user_id, issue_id=78802,
6230 closed_timestamp=base_date + 15)
6231 self.services.issue.TestAddIssue(issue2)
6232 issue3 = fake.MakeTestIssue(
6233 789, 3, 'sum', 'New', self.user_3.user_id, issue_id=78803,
6234 closed_timestamp=base_date + 10,
6235 labels=['Restrict-View-Sheep']) # user_1 does not have 'Sheep' perms
6236 self.services.issue.TestAddIssue(issue3)
6237 hotlist_item_tuples = [
6238 (issue1.issue_id, 1, self.user_1.user_id, base_date + 2, 'dude wheres'),
6239 (issue3.issue_id, 21, self.user_2.user_id, base_date, ''),
6240 (issue2.issue_id, 31, self.user_1.user_id, base_date + 1, 'my car')]
6241 hotlist = self.work_env.services.features.TestAddHotlist(
6242 'hotlist', summary='Summary', description='Description',
6243 owner_ids=owner_ids, hotlist_id=123,
6244 hotlist_item_fields=hotlist_item_tuples)
6245
6246 self.SignIn(user_id=self.user_1.user_id)
6247 with self.work_env as we:
6248 max_items = 3
6249 start = 0
6250 can = 1
6251 sort_spec = 'rank'
6252 group_by_spec = ''
6253 list_result = we.ListHotlistItems(
6254 hotlist.hotlist_id, max_items, start, can, sort_spec, group_by_spec)
6255
6256 expected_items = [
6257 features_pb2.Hotlist.HotlistItem(
6258 issue_id=issue1.issue_id, rank=1, adder_id=self.user_1.user_id,
6259 date_added=base_date + 2, note='dude wheres'),
6260 features_pb2.Hotlist.HotlistItem(
6261 issue_id=issue2.issue_id, rank=31, adder_id=self.user_1.user_id,
6262 date_added=base_date + 1, note='my car')]
6263 self.assertEqual(list_result.items, expected_items)
6264
6265 self.assertIsNone(list_result.next_start)
6266
6267 def testTransferHotlistOwnership(self):
6268 """We can transfer ownership of a hotlist."""
6269 owner_ids = [self.user_1.user_id]
6270 editor_ids = [self.user_2.user_id]
6271 hotlist = self.work_env.services.features.TestAddHotlist(
6272 'hotlist', summary='Summary', description='Description',
6273 owner_ids=owner_ids, editor_ids=editor_ids, hotlist_id=123)
6274
6275 self.SignIn(user_id=self.user_1.user_id)
6276 with self.work_env as we:
6277 we.TransferHotlistOwnership(
6278 hotlist.hotlist_id, self.user_2.user_id, True)
6279 transferred_hotlist = we.GetHotlist(hotlist.hotlist_id)
6280 self.assertEqual(transferred_hotlist.owner_ids, editor_ids)
6281 self.assertEqual(transferred_hotlist.editor_ids, owner_ids)
6282
6283 def testTransferHotlistOwnership_NoPermission(self):
6284 """We only let hotlist owners transfer hotlist ownership."""
6285 owner_ids = [self.user_1.user_id]
6286 editor_ids = [self.user_2.user_id]
6287 hotlist = self.work_env.services.features.TestAddHotlist(
6288 'SameName', summary='Summary', description='Description',
6289 owner_ids=owner_ids, editor_ids=editor_ids, hotlist_id=123)
6290
6291 self.SignIn(user_id=self.user_2.user_id)
6292 with self.assertRaises(permissions.PermissionException):
6293 with self.work_env as we:
6294 we.TransferHotlistOwnership(
6295 hotlist.hotlist_id, self.user_2.user_id, True)
6296
6297 def testTransferHotlistOwnership_RejectNewOwner(self):
6298 """We reject attempts when new owner already owns a
6299 hotlist with the same name."""
6300 owner_ids = [self.user_1.user_id]
6301 hotlist = self.work_env.services.features.TestAddHotlist(
6302 'SameName', summary='Summary', description='Description',
6303 owner_ids=owner_ids, hotlist_id=123)
6304 _other_hotlist = self.work_env.services.features.TestAddHotlist(
6305 'SameName', summary='summary', description='description',
6306 owner_ids=[self.user_2.user_id], hotlist_id=124)
6307
6308 self.SignIn(user_id=self.user_1.user_id)
6309 with self.assertRaises(exceptions.InputException):
6310 with self.work_env as we:
6311 we.TransferHotlistOwnership(
6312 hotlist.hotlist_id, self.user_2.user_id, True)
6313
6314 def testRemoveHotlistEditors(self):
6315 """Hotlist owner can remove editors as normal."""
6316 owner_ids = [self.user_1.user_id]
6317 editor_ids = [self.user_2.user_id]
6318 hotlist = self.work_env.services.features.TestAddHotlist(
6319 'RejectUnowned',
6320 summary='Summary',
6321 description='description',
6322 owner_ids=owner_ids,
6323 editor_ids=editor_ids,
6324 hotlist_id=1257)
6325
6326 self.SignIn(user_id=self.user_1.user_id)
6327 with self.work_env as we:
6328 remove_editor_ids = [self.user_2.user_id]
6329 we.RemoveHotlistEditors(hotlist.hotlist_id, remove_editor_ids)
6330
6331 updated_hotlist = we.GetHotlist(hotlist.hotlist_id)
6332 self.assertEqual(updated_hotlist.owner_ids, owner_ids)
6333 self.assertEqual(updated_hotlist.editor_ids, [])
6334
6335 def testRemoveHotlistEditors_NoPermission(self):
6336 """A user who is not in the hotlist cannot remove editors."""
6337 owner_ids = [self.user_1.user_id]
6338 editor_ids = [self.user_2.user_id]
6339 hotlist = self.work_env.services.features.TestAddHotlist(
6340 'RejectUnowned',
6341 summary='Summary',
6342 description='description',
6343 owner_ids=owner_ids,
6344 editor_ids=editor_ids,
6345 hotlist_id=1257)
6346
6347 self.SignIn(user_id=self.user_3.user_id)
6348 with self.assertRaises(permissions.PermissionException):
6349 with self.work_env as we:
6350 remove_editor_ids = [self.user_2.user_id]
6351 we.RemoveHotlistEditors(hotlist.hotlist_id, remove_editor_ids)
6352
6353 def testRemoveHotlistEditors_CannotRemoveOtherEditors(self):
6354 """A user who is not the hotlist owner cannot remove editors."""
6355 owner_ids = [self.user_1.user_id]
6356 editor_ids = [self.user_2.user_id, self.user_3.user_id]
6357 hotlist = self.work_env.services.features.TestAddHotlist(
6358 'RejectUnowned',
6359 summary='Summary',
6360 description='description',
6361 owner_ids=owner_ids,
6362 editor_ids=editor_ids,
6363 hotlist_id=1257)
6364
6365 self.SignIn(user_id=self.user_3.user_id)
6366 with self.assertRaises(permissions.PermissionException):
6367 with self.work_env as we:
6368 remove_editor_ids = [self.user_2.user_id]
6369 we.RemoveHotlistEditors(hotlist.hotlist_id, remove_editor_ids)
6370
6371 def testRemoveHotlistEditors_AllowRemoveSelf(self):
6372 """A non-owner member of a hotlist can remove themselves."""
6373 owner_ids = [self.user_1.user_id]
6374 editor_ids = [self.user_2.user_id]
6375 hotlist = self.work_env.services.features.TestAddHotlist(
6376 'RejectUnowned',
6377 summary='Summary',
6378 description='description',
6379 owner_ids=owner_ids,
6380 editor_ids=editor_ids,
6381 hotlist_id=1257)
6382
6383 self.SignIn(user_id=self.user_2.user_id)
6384
6385 with self.work_env as we:
6386 remove_editor_ids = [self.user_2.user_id]
6387 we.RemoveHotlistEditors(hotlist.hotlist_id, remove_editor_ids)
6388
6389 updated_hotlist = we.GetHotlist(hotlist.hotlist_id)
6390 self.assertEqual(updated_hotlist.owner_ids, owner_ids)
6391 self.assertEqual(updated_hotlist.editor_ids, [])
6392
6393 # assert cannot remove someone else
6394 with self.assertRaises(permissions.PermissionException):
6395 with self.work_env as we:
6396 we.RemoveHotlistEditors(hotlist.hotlist_id, [self.user_3.user_id])
6397
6398 def testRemoveHotlistEditors_AllowRemoveParentLinkedAccount(self):
6399 """A non-owner member of a hotlist can remove their linked accounts."""
6400 owner_ids = [self.user_1.user_id]
6401 editor_ids = [self.user_3.user_id]
6402 hotlist = self.work_env.services.features.TestAddHotlist(
6403 'RejectUnowned',
6404 summary='Summary',
6405 description='description',
6406 owner_ids=owner_ids,
6407 editor_ids=editor_ids,
6408 hotlist_id=1257)
6409 self.services.user.InviteLinkedParent(
6410 self.cnxn, self.user_3.user_id, self.user_2.user_id)
6411 self.services.user.AcceptLinkedChild(
6412 self.cnxn, self.user_3.user_id, self.user_2.user_id)
6413
6414 self.SignIn(user_id=self.user_2.user_id)
6415 with self.work_env as we:
6416 remove_editor_ids = [self.user_3.user_id]
6417 we.RemoveHotlistEditors(hotlist.hotlist_id, remove_editor_ids)
6418
6419 updated_hotlist = we.GetHotlist(hotlist.hotlist_id)
6420 self.assertEqual(updated_hotlist.owner_ids, owner_ids)
6421 self.assertEqual(updated_hotlist.editor_ids, [])
6422
6423 def testRemoveHotlistEditors_AllowRemoveChildLinkedAccount(self):
6424 """A non-owner member of a hotlist can remove their linked accounts."""
6425 owner_ids = [self.user_1.user_id]
6426 editor_ids = [self.user_2.user_id]
6427 hotlist = self.work_env.services.features.TestAddHotlist(
6428 'RejectUnowned',
6429 summary='Summary',
6430 description='description',
6431 owner_ids=owner_ids,
6432 editor_ids=editor_ids,
6433 hotlist_id=1257)
6434 self.services.user.InviteLinkedParent(
6435 self.cnxn, self.user_3.user_id, self.user_2.user_id)
6436 self.services.user.AcceptLinkedChild(
6437 self.cnxn, self.user_3.user_id, self.user_2.user_id)
6438
6439 self.SignIn(user_id=self.user_3.user_id)
6440 with self.work_env as we:
6441 remove_editor_ids = [self.user_2.user_id]
6442 we.RemoveHotlistEditors(hotlist.hotlist_id, remove_editor_ids)
6443
6444 updated_hotlist = we.GetHotlist(hotlist.hotlist_id)
6445 self.assertEqual(updated_hotlist.owner_ids, owner_ids)
6446 self.assertEqual(updated_hotlist.editor_ids, [])
6447
6448 def testDeleteHotlist(self):
6449 hotlist = self.work_env.services.features.CreateHotlist(
6450 self.cnxn, 'hotlistName', 'summary', 'desc', [444], [])
6451
6452 self.SignIn(user_id=444)
6453 with self.work_env as we:
6454 we.DeleteHotlist(hotlist.hotlist_id)
6455
6456 # Just test that services.features.ExpungeHotlists was called
6457 self.assertTrue(
6458 hotlist.hotlist_id in self.services.features.expunged_hotlist_ids)
6459
6460 def testDeleteHotlist_NoPerms(self):
6461 hotlist = self.work_env.services.features.CreateHotlist(
6462 self.cnxn, 'hotlistName', 'summary', 'desc', [444], [])
6463
6464 self.SignIn(user_id=333)
6465 with self.assertRaises(permissions.PermissionException):
6466 with self.work_env as we:
6467 we.DeleteHotlist(hotlist.hotlist_id)
6468
6469 def testListHotlistsByUser_Normal(self):
6470 self.work_env.services.features.CreateHotlist(
6471 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6472 owner_ids=[444], editor_ids=[])
6473
6474 self.SignIn()
6475 with self.work_env as we:
6476 hotlists = we.ListHotlistsByUser(444)
6477
6478 self.assertEqual(1, len(hotlists))
6479 hotlist = hotlists[0]
6480 self.assertEqual([444], hotlist.owner_ids)
6481 self.assertEqual([], hotlist.editor_ids)
6482 self.assertEqual('Fake-Hotlist', hotlist.name)
6483 self.assertEqual('Summary', hotlist.summary)
6484 self.assertEqual('Description', hotlist.description)
6485
6486 def testListHotlistsByUser_AnotherUser(self):
6487 self.work_env.services.features.CreateHotlist(
6488 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6489 owner_ids=[333], editor_ids=[])
6490
6491 self.SignIn()
6492 with self.work_env as we:
6493 hotlists = we.ListHotlistsByUser(333)
6494
6495 self.assertEqual(1, len(hotlists))
6496 hotlist = hotlists[0]
6497 self.assertEqual([333], hotlist.owner_ids)
6498 self.assertEqual([], hotlist.editor_ids)
6499 self.assertEqual('Fake-Hotlist', hotlist.name)
6500 self.assertEqual('Summary', hotlist.summary)
6501 self.assertEqual('Description', hotlist.description)
6502
6503 def testListHotlistsByUser_NotSignedIn(self):
6504 self.work_env.services.features.CreateHotlist(
6505 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6506 owner_ids=[444], editor_ids=[])
6507
6508 with self.work_env as we:
6509 hotlists = we.ListHotlistsByUser(444)
6510
6511 self.assertEqual(1, len(hotlists))
6512 hotlist = hotlists[0]
6513 self.assertEqual([444], hotlist.owner_ids)
6514 self.assertEqual([], hotlist.editor_ids)
6515 self.assertEqual('Fake-Hotlist', hotlist.name)
6516 self.assertEqual('Summary', hotlist.summary)
6517 self.assertEqual('Description', hotlist.description)
6518
6519 def testListHotlistsByUser_NoUserId(self):
6520 with self.assertRaises(exceptions.InputException):
6521 with self.work_env as we:
6522 we.ListHotlistsByUser(None)
6523
6524
6525 def testListHotlistsByUser_Empty(self):
6526 self.work_env.services.features.CreateHotlist(
6527 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6528 owner_ids=[333], editor_ids=[])
6529
6530 self.SignIn()
6531 with self.work_env as we:
6532 hotlists = we.ListHotlistsByUser(444)
6533
6534 self.assertEqual(0, len(hotlists))
6535
6536 def testListHotlistsByUser_NoHotlists(self):
6537 self.SignIn()
6538 with self.work_env as we:
6539 hotlists = we.ListHotlistsByUser(444)
6540
6541 self.assertEqual(0, len(hotlists))
6542
6543 def testListHotlistsByUser_PrivateHotlistAsOwner(self):
6544 self.work_env.services.features.CreateHotlist(
6545 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6546 owner_ids=[111], editor_ids=[333], is_private=True)
6547
6548 self.SignIn()
6549 with self.work_env as we:
6550 hotlists = we.ListHotlistsByUser(333)
6551
6552 self.assertEqual(1, len(hotlists))
6553 hotlist = hotlists[0]
6554 self.assertEqual([111], hotlist.owner_ids)
6555 self.assertEqual([333], hotlist.editor_ids)
6556 self.assertEqual('Fake-Hotlist', hotlist.name)
6557 self.assertEqual('Summary', hotlist.summary)
6558 self.assertEqual('Description', hotlist.description)
6559
6560 def testListHotlistsByUser_PrivateHotlistAsEditor(self):
6561 self.work_env.services.features.CreateHotlist(
6562 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6563 owner_ids=[333], editor_ids=[111], is_private=True)
6564
6565 self.SignIn()
6566 with self.work_env as we:
6567 hotlists = we.ListHotlistsByUser(333)
6568
6569 self.assertEqual(1, len(hotlists))
6570 hotlist = hotlists[0]
6571 self.assertEqual([333], hotlist.owner_ids)
6572 self.assertEqual([111], hotlist.editor_ids)
6573 self.assertEqual('Fake-Hotlist', hotlist.name)
6574 self.assertEqual('Summary', hotlist.summary)
6575 self.assertEqual('Description', hotlist.description)
6576
6577 def testListHotlistsByUser_PrivateHotlistNoAcess(self):
6578 self.work_env.services.features.CreateHotlist(
6579 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6580 owner_ids=[333], editor_ids=[], is_private=True)
6581
6582 self.SignIn()
6583 with self.work_env as we:
6584 hotlists = we.ListHotlistsByUser(333)
6585
6586 self.assertEqual(0, len(hotlists))
6587
6588 def testListHotlistsByIssue_Normal(self):
6589 issue = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
6590 self.services.issue.TestAddIssue(issue)
6591 hotlist = self.work_env.services.features.CreateHotlist(
6592 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6593 owner_ids=[111], editor_ids=[])
6594 self.AddIssueToHotlist(hotlist.hotlist_id)
6595
6596 self.SignIn()
6597 with self.work_env as we:
6598 hotlists = we.ListHotlistsByIssue(78901)
6599
6600 self.assertEqual(1, len(hotlists))
6601 hotlist = hotlists[0]
6602 self.assertEqual([111], hotlist.owner_ids)
6603 self.assertEqual([], hotlist.editor_ids)
6604 self.assertEqual('Fake-Hotlist', hotlist.name)
6605 self.assertEqual('Summary', hotlist.summary)
6606 self.assertEqual('Description', hotlist.description)
6607
6608 def testListHotlistsByIssue_NotSignedIn(self):
6609 issue = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
6610 self.services.issue.TestAddIssue(issue)
6611 hotlist = self.work_env.services.features.CreateHotlist(
6612 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6613 owner_ids=[111], editor_ids=[])
6614 self.AddIssueToHotlist(hotlist.hotlist_id)
6615
6616 with self.work_env as we:
6617 hotlists = we.ListHotlistsByIssue(78901)
6618
6619 self.assertEqual(1, len(hotlists))
6620 hotlist = hotlists[0]
6621 self.assertEqual([111], hotlist.owner_ids)
6622 self.assertEqual([], hotlist.editor_ids)
6623 self.assertEqual('Fake-Hotlist', hotlist.name)
6624 self.assertEqual('Summary', hotlist.summary)
6625 self.assertEqual('Description', hotlist.description)
6626
6627 def testListHotlistsByIssue_NotAllowedToSeeIssue(self):
6628 issue = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
6629 issue.labels = ['Restrict-View-CoreTeam']
6630 self.services.issue.TestAddIssue(issue)
6631 hotlist = self.work_env.services.features.CreateHotlist(
6632 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6633 owner_ids=[111], editor_ids=[])
6634 self.AddIssueToHotlist(hotlist.hotlist_id)
6635
6636 # We should get a permission exception
6637 self.SignIn(333)
6638 with self.assertRaises(permissions.PermissionException):
6639 with self.work_env as we:
6640 we.ListHotlistsByIssue(78901)
6641
6642 def testListHotlistsByIssue_NoSuchIssue(self):
6643 self.SignIn()
6644 with self.assertRaises(exceptions.NoSuchIssueException):
6645 with self.work_env as we:
6646 we.ListHotlistsByIssue(78901)
6647
6648 def testListHotlistsByIssue_NoHotlists(self):
6649 issue = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
6650 self.services.issue.TestAddIssue(issue)
6651
6652 self.SignIn()
6653 with self.work_env as we:
6654 hotlists = we.ListHotlistsByIssue(78901)
6655
6656 self.assertEqual(0, len(hotlists))
6657
6658 def testListHotlistsByIssue_PrivateHotlistAsOwner(self):
6659 issue = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
6660 self.services.issue.TestAddIssue(issue)
6661 hotlist = self.work_env.services.features.CreateHotlist(
6662 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6663 owner_ids=[111], editor_ids=[333], is_private=True)
6664 self.AddIssueToHotlist(hotlist.hotlist_id)
6665
6666 self.SignIn()
6667 with self.work_env as we:
6668 hotlists = we.ListHotlistsByIssue(78901)
6669
6670 self.assertEqual(1, len(hotlists))
6671 hotlist = hotlists[0]
6672 self.assertEqual([111], hotlist.owner_ids)
6673 self.assertEqual([333], hotlist.editor_ids)
6674 self.assertEqual('Fake-Hotlist', hotlist.name)
6675 self.assertEqual('Summary', hotlist.summary)
6676 self.assertEqual('Description', hotlist.description)
6677
6678 def testListHotlistsByIssue_PrivateHotlistAsEditor(self):
6679 issue = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
6680 self.services.issue.TestAddIssue(issue)
6681 hotlist = self.work_env.services.features.CreateHotlist(
6682 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6683 owner_ids=[333], editor_ids=[111], is_private=True)
6684 self.AddIssueToHotlist(hotlist.hotlist_id)
6685
6686 self.SignIn()
6687 with self.work_env as we:
6688 hotlists = we.ListHotlistsByIssue(78901)
6689
6690 self.assertEqual(1, len(hotlists))
6691 hotlist = hotlists[0]
6692 self.assertEqual([333], hotlist.owner_ids)
6693 self.assertEqual([111], hotlist.editor_ids)
6694 self.assertEqual('Fake-Hotlist', hotlist.name)
6695 self.assertEqual('Summary', hotlist.summary)
6696 self.assertEqual('Description', hotlist.description)
6697
6698 def testListHotlistsByIssue_PrivateHotlistNoAcess(self):
6699 issue = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
6700 self.services.issue.TestAddIssue(issue)
6701 hotlist = self.work_env.services.features.CreateHotlist(
6702 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6703 owner_ids=[444], editor_ids=[333], is_private=True)
6704 self.AddIssueToHotlist(hotlist.hotlist_id)
6705
6706 self.SignIn()
6707 with self.work_env as we:
6708 hotlists = we.ListHotlistsByIssue(78901)
6709
6710 self.assertEqual(0, len(hotlists))
6711
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01006712 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +00006713 def testListRecentlyVisitedHotlists(self):
6714 hotlists = [
6715 self.work_env.services.features.CreateHotlist(
6716 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6717 owner_ids=[444], editor_ids=[111]),
6718 self.work_env.services.features.CreateHotlist(
6719 self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
6720 owner_ids=[111], editor_ids=[333]),
6721 self.work_env.services.features.CreateHotlist(
6722 self.cnxn, 'Private-Hotlist', 'Summary', 'Description',
6723 owner_ids=[111], editor_ids=[333], is_private=True),
6724 self.work_env.services.features.CreateHotlist(
6725 self.cnxn, 'Private-Hotlist-2', 'Summary', 'Description',
6726 owner_ids=[222], editor_ids=[333], is_private=True)]
6727
6728 for hotlist in hotlists:
6729 self.work_env.services.user.AddVisitedHotlist(
6730 self.cnxn, 111, hotlist.hotlist_id)
6731
6732 self.SignIn()
6733 with self.work_env as we:
6734 visited_hotlists = we.ListRecentlyVisitedHotlists()
6735
6736 # We don't have permission to see the last hotlist, because it is marked as
6737 # private and we're not owners or editors of it.
6738 self.assertEqual(hotlists[:-1], visited_hotlists)
6739
6740 def testListRecentlyVisitedHotlists_Anon(self):
6741 with self.work_env as we:
6742 self.assertEqual([], we.ListRecentlyVisitedHotlists())
6743
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01006744 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +00006745 def testListStarredHotlists(self):
6746 hotlists = [
6747 self.work_env.services.features.CreateHotlist(
6748 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6749 owner_ids=[444], editor_ids=[111]),
6750 self.work_env.services.features.CreateHotlist(
6751 self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
6752 owner_ids=[111], editor_ids=[333]),
6753 self.work_env.services.features.CreateHotlist(
6754 self.cnxn, 'Private-Hotlist', 'Summary', 'Description',
6755 owner_ids=[111], editor_ids=[333], is_private=True),
6756 self.work_env.services.features.CreateHotlist(
6757 self.cnxn, 'Private-Hotlist-2', 'Summary', 'Description',
6758 owner_ids=[222], editor_ids=[333], is_private=True)]
6759
6760 for hotlist in hotlists:
6761 self.work_env.services.hotlist_star.SetStar(
6762 self.cnxn, hotlist.hotlist_id, 111, True)
6763
6764 self.SignIn()
6765 with self.work_env as we:
6766 visited_hotlists = we.ListStarredHotlists()
6767
6768 # We don't have permission to see the last hotlist, because it is marked as
6769 # private and we're not owners or editors of it.
6770 self.assertEqual(hotlists[:-1], visited_hotlists)
6771
6772 def testListStarredHotlists_Anon(self):
6773 with self.work_env as we:
6774 self.assertEqual([], we.ListStarredHotlists())
6775
6776 def testStarHotlist_Normal(self):
6777 """We can star and unstar a hotlist."""
6778 hotlist_id = self.work_env.services.features.CreateHotlist(
6779 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6780 owner_ids=[111], editor_ids=[]).hotlist_id
6781
6782 self.SignIn()
6783 with self.work_env as we:
6784 self.assertFalse(we.IsHotlistStarred(hotlist_id))
6785 we.StarHotlist(hotlist_id, True)
6786 self.assertTrue(we.IsHotlistStarred(hotlist_id))
6787 we.StarHotlist(hotlist_id, False)
6788 self.assertFalse(we.IsHotlistStarred(hotlist_id))
6789
6790 def testStarHotlist_NoHotlistSpecified(self):
6791 """A hotlist must be specified."""
6792 self.SignIn()
6793 with self.assertRaises(exceptions.InputException):
6794 with self.work_env as we:
6795 we.StarHotlist(None, True)
6796
6797 def testStarHotlist_NoSuchHotlist(self):
6798 """We can't star a nonexistent hotlist."""
6799 self.SignIn()
6800 with self.assertRaises(features_svc.NoSuchHotlistException):
6801 with self.work_env as we:
6802 we.StarHotlist(999, True)
6803
6804 def testStarHotlist_Anon(self):
6805 """Anon user can't star a hotlist."""
6806 with self.assertRaises(exceptions.InputException):
6807 with self.work_env as we:
6808 we.StarHotlist(999, True)
6809
6810 # testIsHotlistStarred_Normal is Tested by method testStarHotlist_Normal().
6811
6812 def testIsHotlistStarred_Anon(self):
6813 """Anon user can't star a hotlist."""
6814 with self.work_env as we:
6815 self.assertFalse(we.IsHotlistStarred(999))
6816
6817 def testIsHotlistStarred_NoHotlistSpecified(self):
6818 """A Hotlist ID must be specified."""
6819 with self.work_env as we:
6820 with self.assertRaises(exceptions.InputException):
6821 we.IsHotlistStarred(None)
6822
6823 def testIsHotlistStarred_NoSuchHotlist(self):
6824 """We can't check for stars on a nonexistent hotlist."""
6825 self.SignIn()
6826 with self.assertRaises(features_svc.NoSuchHotlistException):
6827 with self.work_env as we:
6828 we.IsHotlistStarred(999)
6829
6830 def testGetHotlistStarCount(self):
6831 hotlist = self.work_env.services.features.CreateHotlist(
6832 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6833 owner_ids=[111], editor_ids=[])
6834 self.services.hotlist_star.SetStar(
6835 self.cnxn, hotlist.hotlist_id, 111, True)
6836 self.services.hotlist_star.SetStar(
6837 self.cnxn, hotlist.hotlist_id, 222, True)
6838
6839 with self.work_env as we:
6840 self.assertEqual(2, we.GetHotlistStarCount(hotlist.hotlist_id))
6841
6842 def testGetHotlistStarCount_NoneHotlist(self):
6843 with self.assertRaises(exceptions.InputException):
6844 with self.work_env as we:
6845 we.GetHotlistStarCount(None)
6846
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01006847 @pytest.mark.skip(reason='Test is flaky (https://crbug.com/monorail/12052)')
Copybara854996b2021-09-07 19:36:02 +00006848 def testGetHotlistStarCount_NoSuchHotlist(self):
6849 with self.assertRaises(features_svc.NoSuchHotlistException):
6850 with self.work_env as we:
6851 we.GetHotlistStarCount(123)
6852
6853 def testCheckHotlistName_OK(self):
6854 self.SignIn()
6855 with self.work_env as we:
6856 error = we.CheckHotlistName('Fake-Hotlist')
6857 self.assertIsNone(error)
6858
6859 def testCheckHotlistName_Anon(self):
6860 with self.assertRaises(exceptions.InputException):
6861 with self.work_env as we:
6862 we.CheckHotlistName('Fake-Hotlist')
6863
6864 def testCheckHotlistName_InvalidName(self):
6865 self.SignIn()
6866 with self.work_env as we:
6867 error = we.CheckHotlistName('**Invalid**')
6868 self.assertIsNotNone(error)
6869
6870 def testCheckHotlistName_AlreadyExists(self):
6871 self.work_env.services.features.CreateHotlist(
6872 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6873 owner_ids=[111], editor_ids=[])
6874
6875 self.SignIn()
6876 with self.work_env as we:
6877 error = we.CheckHotlistName('Fake-Hotlist')
6878 self.assertIsNotNone(error)
6879
6880 def testRemoveIssuesFromHotlists(self):
6881 """We can remove issues from hotlists."""
6882 issue1 = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
6883 self.services.issue.TestAddIssue(issue1)
6884 issue2 = fake.MakeTestIssue(789, 2, 'sum2', 'New', 111, issue_id=78902)
6885 self.services.issue.TestAddIssue(issue2)
6886
6887 hotlist1 = self.work_env.services.features.CreateHotlist(
6888 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6889 owner_ids=[111], editor_ids=[])
6890 self.AddIssueToHotlist(hotlist1.hotlist_id, issue1.issue_id)
6891 self.AddIssueToHotlist(hotlist1.hotlist_id, issue2.issue_id)
6892
6893 hotlist2 = self.work_env.services.features.CreateHotlist(
6894 self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
6895 owner_ids=[111], editor_ids=[])
6896 self.AddIssueToHotlist(hotlist2.hotlist_id, issue1.issue_id)
6897
6898 self.SignIn()
6899 with self.work_env as we:
6900 we.RemoveIssuesFromHotlists(
6901 [hotlist1.hotlist_id, hotlist2.hotlist_id], [issue1.issue_id])
6902
6903 self.assertEqual(
6904 [issue2.issue_id], [item.issue_id for item in hotlist1.items])
6905 self.assertEqual(0, len(hotlist2.items))
6906
6907 def testRemoveIssuesFromHotlists_RemoveIssueNotInHotlist(self):
6908 """Removing an issue from a hotlist that doesn't have it has no effect."""
6909 issue1 = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
6910 self.services.issue.TestAddIssue(issue1)
6911 issue2 = fake.MakeTestIssue(789, 2, 'sum2', 'New', 111, issue_id=78902)
6912 self.services.issue.TestAddIssue(issue2)
6913
6914 hotlist1 = self.work_env.services.features.CreateHotlist(
6915 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6916 owner_ids=[111], editor_ids=[])
6917 self.AddIssueToHotlist(hotlist1.hotlist_id, issue1.issue_id)
6918 self.AddIssueToHotlist(hotlist1.hotlist_id, issue2.issue_id)
6919
6920 hotlist2 = self.work_env.services.features.CreateHotlist(
6921 self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
6922 owner_ids=[111], editor_ids=[])
6923 self.AddIssueToHotlist(hotlist2.hotlist_id, issue1.issue_id)
6924
6925 self.SignIn()
6926 with self.work_env as we:
6927 # Issue 2 is not in Fake-Hotlist-2
6928 we.RemoveIssuesFromHotlists([hotlist2.hotlist_id], [issue2.issue_id])
6929
6930 self.assertEqual(
6931 [issue1.issue_id, issue2.issue_id],
6932 [item.issue_id for item in hotlist1.items])
6933 self.assertEqual(
6934 [issue1.issue_id],
6935 [item.issue_id for item in hotlist2.items])
6936
6937 def testRemoveIssuesFromHotlists_NotAllowed(self):
6938 """Only owners and editors can remove issues."""
6939 hotlist = self.work_env.services.features.CreateHotlist(
6940 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6941 owner_ids=[111], editor_ids=[])
6942
6943 # 333 is not an owner or editor.
6944 self.SignIn(333)
6945 with self.assertRaises(permissions.PermissionException):
6946 with self.work_env as we:
6947 we.RemoveIssuesFromHotlists([hotlist.hotlist_id], [1234])
6948
6949 def testRemoveIssuesFromHotlists_NoSuchHotlist(self):
6950 """We can't remove issues from non existent hotlists."""
6951 with self.assertRaises(features_svc.NoSuchHotlistException):
6952 with self.work_env as we:
6953 we.RemoveIssuesFromHotlists([1, 2, 3], [4, 5, 6])
6954
6955 def testAddIssuesToHotlists(self):
6956 """We can add issues to hotlists."""
6957 issue1 = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
6958 self.services.issue.TestAddIssue(issue1)
6959 issue2 = fake.MakeTestIssue(789, 2, 'sum2', 'New', 111, issue_id=78902)
6960 self.services.issue.TestAddIssue(issue2)
6961
6962 hotlist1 = self.work_env.services.features.CreateHotlist(
6963 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6964 owner_ids=[111], editor_ids=[])
6965 hotlist2 = self.work_env.services.features.CreateHotlist(
6966 self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
6967 owner_ids=[111], editor_ids=[])
6968
6969 self.SignIn()
6970 with self.work_env as we:
6971 we.AddIssuesToHotlists(
6972 [hotlist1.hotlist_id, hotlist2.hotlist_id],
6973 [issue1.issue_id, issue2.issue_id],
6974 'Foo')
6975
6976 self.assertEqual(
6977 [issue1.issue_id, issue2.issue_id],
6978 [item.issue_id for item in hotlist1.items])
6979 self.assertEqual(
6980 [issue1.issue_id, issue2.issue_id],
6981 [item.issue_id for item in hotlist2.items])
6982
6983 self.assertEqual(['Foo', 'Foo'], [item.note for item in hotlist1.items])
6984 self.assertEqual(['Foo', 'Foo'], [item.note for item in hotlist2.items])
6985
6986 def testAddIssuesToHotlists_IssuesAlreadyInHotlist(self):
6987 """Adding an issue to a hotlist that already has it has no effect."""
6988 issue1 = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
6989 self.services.issue.TestAddIssue(issue1)
6990 issue2 = fake.MakeTestIssue(789, 2, 'sum2', 'New', 111, issue_id=78902)
6991 self.services.issue.TestAddIssue(issue2)
6992
6993 hotlist1 = self.work_env.services.features.CreateHotlist(
6994 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
6995 owner_ids=[111], editor_ids=[])
6996 self.AddIssueToHotlist(hotlist1.hotlist_id, issue1.issue_id)
6997 self.AddIssueToHotlist(hotlist1.hotlist_id, issue2.issue_id)
6998
6999 hotlist2 = self.work_env.services.features.CreateHotlist(
7000 self.cnxn, 'Fake-Hotlist-2', 'Summary', 'Description',
7001 owner_ids=[111], editor_ids=[])
7002 self.AddIssueToHotlist(hotlist2.hotlist_id, issue1.issue_id)
7003
7004 self.SignIn()
7005 with self.work_env as we:
7006 # Issue 1 is in both hotlists
7007 we.AddIssuesToHotlists(
7008 [hotlist1.hotlist_id, hotlist2.hotlist_id], [issue1.issue_id], None)
7009
7010 self.assertEqual(
7011 [issue1.issue_id, issue2.issue_id],
7012 [item.issue_id for item in hotlist1.items])
7013 self.assertEqual(
7014 [issue1.issue_id],
7015 [item.issue_id for item in hotlist2.items])
7016
7017 def testAddIssuesToHotlists_NotViewable(self):
7018 """Users can add viewable issues to hotlists."""
7019 issue1 = fake.MakeTestIssue(
7020 789, 1, 'sum1', 'New', 111, issue_id=78901)
7021 issue1.labels = ['Restrict-View-CoreTeam']
7022 self.services.issue.TestAddIssue(issue1)
7023 hotlist = self.work_env.services.features.CreateHotlist(
7024 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
7025 owner_ids=[333], editor_ids=[])
7026
7027 self.SignIn(user_id=333)
7028 with self.assertRaises(permissions.PermissionException):
7029 with self.work_env as we:
7030 we.AddIssuesToHotlists([hotlist.hotlist_id], [78901], None)
7031
7032 def testAddIssuesToHotlists_NotAllowed(self):
7033 """Only owners and editors can add issues."""
7034 hotlist = self.work_env.services.features.CreateHotlist(
7035 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
7036 owner_ids=[111], editor_ids=[])
7037
7038 # 333 is not an owner or editor.
7039 self.SignIn(user_id=333)
7040 with self.assertRaises(permissions.PermissionException):
7041 with self.work_env as we:
7042 we.AddIssuesToHotlists([hotlist.hotlist_id], [1234], None)
7043
7044 def testAddIssuesToHotlists_NoSuchHotlist(self):
7045 """We can't remove issues from non existent hotlists."""
7046 with self.assertRaises(features_svc.NoSuchHotlistException):
7047 with self.work_env as we:
7048 we.AddIssuesToHotlists([1, 2, 3], [4, 5, 6], None)
7049
7050 def createHotlistWithItems(self):
7051 issue_1 = fake.MakeTestIssue(789, 1, 'sum', 'New', 111, issue_id=78901)
7052 self.services.issue.TestAddIssue(issue_1)
7053 issue_2 = fake.MakeTestIssue(789, 2, 'sum', 'New', 111, issue_id=78902)
7054 self.services.issue.TestAddIssue(issue_2)
7055 issue_3 = fake.MakeTestIssue(789, 3, 'sum', 'New', 111, issue_id=78903)
7056 self.services.issue.TestAddIssue(issue_3)
7057 issue_4 = fake.MakeTestIssue(789, 4, 'sum', 'New', 111, issue_id=78904)
7058 self.services.issue.TestAddIssue(issue_4)
7059 owner_ids = [self.user_1.user_id]
7060 editor_ids = [self.user_2.user_id]
7061 hotlist_items = [
7062 (issue_4.issue_id, 31, self.user_3.user_id, self.PAST_TIME, ''),
7063 (issue_3.issue_id, 21, self.user_1.user_id, self.PAST_TIME, ''),
7064 (issue_2.issue_id, 11, self.user_2.user_id, self.PAST_TIME, ''),
7065 (issue_1.issue_id, 1, self.user_1.user_id, self.PAST_TIME, '')
7066 ]
7067 return self.work_env.services.features.TestAddHotlist(
7068 'HotlistName', owner_ids=owner_ids, editor_ids=editor_ids,
7069 hotlist_item_fields=hotlist_items)
7070
7071 def testRemoveHotlistItems(self):
7072 """We can remove issues from a hotlist."""
7073 hotlist = self.createHotlistWithItems()
7074 self.SignIn(self.user_2.user_id)
7075 with self.work_env as we:
7076 we.RemoveHotlistItems(hotlist.hotlist_id, [78901, 78903])
7077
7078 self.assertEqual([item.issue_id for item in hotlist.items], [78902, 78904])
7079
7080 def testRemoveHotlistItems_NoHotlistPermissions(self):
7081 """We raise an exception if user lacks edit permissions in hotlist."""
7082 self.SignIn(self.user_3.user_id)
7083 with self.assertRaises(permissions.PermissionException):
7084 with self.work_env as we:
7085 we.RemoveHotlistItems(self.hotlist.hotlist_id, [78901])
7086
7087 def testRemoveHotlistItems_NoSuchHotlist(self):
7088 """We raise an exception if the hotlist is not found."""
7089 with self.assertRaises(features_svc.NoSuchHotlistException):
7090 with self.work_env as we:
7091 we.RemoveHotlistItems(self.dne_hotlist_id, [78901])
7092
7093 def testRemoveHotlistItems_ItemNotFound(self):
7094 """We raise an exception if user tries to remove item not in hotlist."""
7095 hotlist = self.createHotlistWithItems()
7096 self.SignIn(self.user_2.user_id)
7097 with self.assertRaises(exceptions.InputException):
7098 with self.work_env as we:
7099 we.RemoveHotlistItems(hotlist.hotlist_id, [404])
7100
7101 def testAddHotlistItems_NoSuchHotlist(self):
7102 """We raise an exception if the hotlist is not found."""
7103 with self.assertRaises(features_svc.NoSuchHotlistException):
7104 with self.work_env as we:
7105 we.AddHotlistItems(self.dne_hotlist_id, [78901], 0)
7106
7107 def testAddHotlistItems_NoHotlistEditPermissions(self):
7108 """We raise an exception if the user lacks edit permissions in hotlist."""
7109 self.SignIn(self.user_3.user_id)
7110 with self.assertRaises(permissions.PermissionException):
7111 with self.work_env as we:
7112 we.AddHotlistItems(self.hotlist.hotlist_id, [78901], 0)
7113
7114 def testAddHotlistItems_NoItemsGiven(self):
7115 """We raise an exception if the given list of issues is empty."""
7116 hotlist = self.createHotlistWithItems()
7117 self.SignIn(self.user_2.user_id)
7118 with self.assertRaises(exceptions.InputException):
7119 with self.work_env as we:
7120 we.AddHotlistItems(hotlist.hotlist_id, [], 0)
7121
7122 def testAddHotlistItems(self):
7123 """We add new items to the hotlist and don't touch existing items."""
7124 hotlist = self.createHotlistWithItems()
7125 self.SignIn(self.user_2.user_id)
7126 with self.work_env as we:
7127 we.AddHotlistItems(hotlist.hotlist_id, [78909, 78910, 78901], 2)
7128
7129 expected_item_ids = [78901, 78902, 78909, 78910, 78903, 78904]
7130 updated_hotlist = we.GetHotlist(hotlist.hotlist_id)
7131 self.assertEqual(
7132 expected_item_ids, [item.issue_id for item in updated_hotlist.items])
7133
7134 def testRerankHotlistItems_NoPerms(self):
7135 """We don't let non editors/owners rerank HotlistItems."""
7136 hotlist = self.createHotlistWithItems()
7137 moved_ids = [78901]
7138 target_position = 0
7139 self.SignIn(self.user_3.user_id)
7140 with self.assertRaises(permissions.PermissionException):
7141 with self.work_env as we:
7142 we.RerankHotlistItems(hotlist.hotlist_id, moved_ids, target_position)
7143
7144 def testRerankHotlistItems_HotlistItemsNotFound(self):
7145 """We raise an exception if not all Issue IDs are in the hotlist."""
7146 hotlist = self.createHotlistWithItems()
7147 # 78909 is not an existing HotlistItem issue.
7148 moved_ids = [78901, 78909]
7149 target_position = 1
7150 self.SignIn(self.user_2.user_id)
7151 with self.assertRaises(exceptions.InputException):
7152 with self.work_env as we:
7153 we.RerankHotlistItems(hotlist.hotlist_id, moved_ids, target_position)
7154
7155 def testRerankHotlistItems_MovedIssuesEmpty(self):
7156 """We raise an exception if the list of Issue IDs is empty."""
7157 hotlist = self.createHotlistWithItems()
7158 moved_ids = []
7159 target_position = 1
7160 self.SignIn(self.user_2.user_id)
7161 with self.assertRaises(exceptions.InputException):
7162 with self.work_env as we:
7163 we.RerankHotlistItems(hotlist.hotlist_id, moved_ids, target_position)
7164
7165 @mock.patch('time.time')
7166 def testRerankHotlistItems(self, fake_time):
7167 """We can rerank HotlistItems."""
7168 fake_time.return_value = self.PAST_TIME
7169 hotlist = self.createHotlistWithItems()
7170 moved_ids = [78901, 78903]
7171 target_position = 1
7172 self.SignIn(self.user_2.user_id)
7173 with self.work_env as we:
7174 updated_hotlist = we.RerankHotlistItems(
7175 hotlist.hotlist_id, moved_ids, target_position)
7176
7177 expected_item_ids = [78902, 78901, 78903, 78904]
7178 self.assertEqual(
7179 expected_item_ids, [item.issue_id for item in updated_hotlist.items])
7180
7181 @mock.patch('time.time')
7182 def testGetChangedHotlistItems(self, fake_time):
7183 """We can get changed HotlistItems when moving existing and new issues."""
7184 fake_time.return_value = self.PAST_TIME
7185 hotlist = self.createHotlistWithItems()
7186 # moved_ids include new issues not in hotlist: [78907, 78909]
7187 moved_ids = [78901, 78907, 78903, 78909]
7188 target_position = 1
7189 self.SignIn(self.user_2.user_id)
7190 with self.work_env as we:
7191 changed_items = we._GetChangedHotlistItems(
7192 hotlist, moved_ids, target_position)
7193
7194 expected_hotlist_items = [
7195 features_pb2.Hotlist.HotlistItem(
7196 issue_id=78901,
7197 rank=14,
7198 note='',
7199 adder_id=self.user_1.user_id,
7200 date_added=self.PAST_TIME),
7201 features_pb2.Hotlist.HotlistItem(
7202 issue_id=78907,
7203 rank=19,
7204 adder_id=self.user_2.user_id,
7205 date_added=self.PAST_TIME),
7206 features_pb2.Hotlist.HotlistItem(
7207 issue_id=78903,
7208 rank=24,
7209 note='',
7210 adder_id=self.user_1.user_id,
7211 date_added=self.PAST_TIME),
7212 features_pb2.Hotlist.HotlistItem(
7213 issue_id=78909,
7214 rank=29,
7215 adder_id=self.user_2.user_id,
7216 date_added=self.PAST_TIME)
7217 ]
7218 self.assertEqual(changed_items, expected_hotlist_items)
7219
7220 # TODO(crbug/monorail/7104): Remove these tests once RerankHotlistIssues
7221 # is deleted.
7222 def testRerankHotlistIssues_SplitAbove(self):
7223 """We can rerank issues in a hotlist with split_above = true."""
7224 owner_ids = [self.user_1.user_id]
7225 editor_ids = [self.user_2.user_id]
7226 follower_ids = []
7227 hotlist_items = [
7228 (78904, 31, self.user_2.user_id, self.PAST_TIME, 'note'),
7229 (78903, 21, self.user_2.user_id, self.PAST_TIME, 'note'),
7230 (78902, 11, self.user_2.user_id, self.PAST_TIME, 'note'),
7231 (78901, 1, self.user_2.user_id, self.PAST_TIME, 'note')]
7232 hotlist = self.work_env.services.features.TestAddHotlist(
7233 'HotlistName', summary='summary', owner_ids=owner_ids,
7234 editor_ids=editor_ids, follower_ids=follower_ids,
7235 hotlist_id=1235, hotlist_item_fields=hotlist_items)
7236
7237 moved_ids = [78901]
7238 target_id = 78904
7239 split_above = True
7240 self.SignIn(self.user_2.user_id)
7241 with self.work_env as we:
7242 we.RerankHotlistIssues(
7243 hotlist.hotlist_id, moved_ids, target_id, split_above)
7244 updated_hotlist = we.GetHotlist(hotlist.hotlist_id)
7245 self.assertEqual(
7246 [item.issue_id for item in updated_hotlist.items],
7247 [78902, 78903, 78901, 78904])
7248
7249 def testRerankHotlistIssues_SplitBelow(self):
7250 """We can rerank issues in a hotlist with split_above = false."""
7251 owner_ids = [self.user_1.user_id]
7252 editor_ids = [self.user_2.user_id]
7253 follower_ids = []
7254 hotlist_items = [
7255 (78904, 31, self.user_2.user_id, self.PAST_TIME, 'note'),
7256 (78903, 21, self.user_2.user_id, self.PAST_TIME, 'note'),
7257 (78902, 11, self.user_2.user_id, self.PAST_TIME, 'note'),
7258 (78901, 1, self.user_2.user_id, self.PAST_TIME, 'note')]
7259 hotlist = self.work_env.services.features.TestAddHotlist(
7260 'HotlistName', summary='summary', owner_ids=owner_ids,
7261 editor_ids=editor_ids, follower_ids=follower_ids,
7262 hotlist_id=1235, hotlist_item_fields=hotlist_items)
7263
7264 moved_ids = [78901]
7265 target_id = 78904
7266 split_above = False
7267 self.SignIn(self.user_2.user_id)
7268 with self.work_env as we:
7269 we.RerankHotlistIssues(
7270 hotlist.hotlist_id, moved_ids, target_id, split_above)
7271 updated_hotlist = we.GetHotlist(hotlist.hotlist_id)
7272 self.assertEqual(
7273 [item.issue_id for item in updated_hotlist.items],
7274 [78902, 78903, 78904, 78901])
7275
7276 def testRerankHotlistIssues_NoPerms(self):
7277 """We don't let non editors/owners update issue ranks."""
7278 owner_ids = [self.user_1.user_id]
7279 editor_ids = []
7280 follower_ids = [self.user_3.user_id]
7281 hotlist = self.work_env.services.features.TestAddHotlist(
7282 'HotlistName', summary='summary', owner_ids=owner_ids,
7283 editor_ids=editor_ids, follower_ids=follower_ids,
7284 hotlist_id=1235)
7285
7286 moved_ids = [78901]
7287 target_id = 78904
7288 split_above = True
7289 self.SignIn(self.user_3.user_id)
7290 with self.assertRaises(permissions.PermissionException):
7291 with self.work_env as we:
7292 we.RerankHotlistIssues(
7293 hotlist.hotlist_id, moved_ids, target_id, split_above)
7294
7295 def testUpdateHotlistIssueNote(self):
7296 issue = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
7297 self.services.issue.TestAddIssue(issue)
7298
7299 hotlist = self.work_env.services.features.CreateHotlist(
7300 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
7301 owner_ids=[111], editor_ids=[])
7302 self.AddIssueToHotlist(hotlist.hotlist_id, issue.issue_id)
7303
7304 self.SignIn()
7305 with self.work_env as we:
7306 we.UpdateHotlistIssueNote(hotlist.hotlist_id, 78901, 'Note')
7307
7308 self.assertEqual('Note', hotlist.items[0].note)
7309
7310 def testUpdateHotlistIssueNote_IssueNotInHotlist(self):
7311 issue = fake.MakeTestIssue(789, 1, 'sum1', 'New', 111, issue_id=78901)
7312 self.services.issue.TestAddIssue(issue)
7313
7314 hotlist = self.work_env.services.features.CreateHotlist(
7315 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
7316 owner_ids=[111], editor_ids=[])
7317
7318 self.SignIn()
7319 with self.assertRaises(exceptions.InputException):
7320 with self.work_env as we:
7321 we.UpdateHotlistIssueNote(hotlist.hotlist_id, 78901, 'Note')
7322
7323 def testUpdateHotlistIssueNote_NoSuchIssue(self):
7324 hotlist = self.work_env.services.features.CreateHotlist(
7325 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
7326 owner_ids=[111], editor_ids=[])
7327
7328 self.SignIn()
7329 with self.assertRaises(exceptions.NoSuchIssueException):
7330 with self.work_env as we:
7331 we.UpdateHotlistIssueNote(hotlist.hotlist_id, 78901, 'Note')
7332
7333 def testUpdateHotlistIssueNote_CantEditHotlist(self):
7334 hotlist = self.work_env.services.features.CreateHotlist(
7335 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
7336 owner_ids=[111], editor_ids=[])
7337
7338 self.SignIn(user_id=333)
7339 with self.assertRaises(permissions.PermissionException):
7340 with self.work_env as we:
7341 we.UpdateHotlistIssueNote(hotlist.hotlist_id, 78901, 'Note')
7342
7343 def testUpdateHotlistIssueNote_NoSuchHotlist(self):
7344 self.SignIn()
7345 with self.assertRaises(features_svc.NoSuchHotlistException):
7346 with self.work_env as we:
7347 we.UpdateHotlistIssueNote(1234, 78901, 'Note')
7348
7349 def testListHotlistPermissions_Anon(self):
7350 hotlist = self.work_env.services.features.CreateHotlist(
7351 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
7352 owner_ids=[self.user_1.user_id], editor_ids=[])
7353 # Anon can view public hotlist.
7354 with self.work_env as we:
7355 anon_perms = we.ListHotlistPermissions(hotlist.hotlist_id)
7356 self.assertEqual(anon_perms, [])
7357
7358 # Anon cannot view private hotlist.
7359 hotlist.is_private = True
7360 with self.assertRaises(permissions.PermissionException):
7361 with self.work_env as we:
7362 we.ListHotlistPermissions(hotlist.hotlist_id)
7363
7364 def testListHotlistPermissions_Owner(self):
7365 hotlist = self.work_env.services.features.CreateHotlist(
7366 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
7367 owner_ids=[self.user_1.user_id], editor_ids=[])
7368
7369 self.SignIn(user_id=self.user_1.user_id)
7370 with self.work_env as we:
7371 owner_perms = we.ListHotlistPermissions(hotlist.hotlist_id)
7372 self.assertEqual(owner_perms, permissions.HOTLIST_OWNER_PERMISSIONS)
7373
7374 def testListHotlistPermissions_Editor(self):
7375 hotlist = self.work_env.services.features.CreateHotlist(
7376 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
7377 owner_ids=[self.user_1.user_id], editor_ids=[self.user_2.user_id])
7378
7379 self.SignIn(user_id=self.user_2.user_id)
7380 with self.work_env as we:
7381 owner_perms = we.ListHotlistPermissions(hotlist.hotlist_id)
7382 self.assertEqual(owner_perms, permissions.HOTLIST_EDITOR_PERMISSIONS)
7383
7384 def testListHotlistPermissions_NonMember(self):
7385 hotlist = self.work_env.services.features.CreateHotlist(
7386 self.cnxn, 'Fake-Hotlist', 'Summary', 'Description',
7387 owner_ids=[self.user_1.user_id], editor_ids=[self.user_2.user_id])
7388
7389 self.SignIn(user_id=self.user_3.user_id)
7390 with self.work_env as we:
7391 perms = we.ListHotlistPermissions(hotlist.hotlist_id)
7392 self.assertEqual(perms, [])
7393
7394 hotlist.is_private = True
7395 with self.assertRaises(permissions.PermissionException):
7396 with self.work_env as we:
7397 we.ListHotlistPermissions(hotlist.hotlist_id)
7398
7399 def testListFieldDefPermissions_Anon(self):
7400 field_id = self.services.config.CreateFieldDef(
7401 self.cnxn, self.project.project_id, 'Field', 'STR_TYPE', None, None,
7402 None, None, None, None, None, None, None, None, None, None, None, None,
7403 [], [])
7404 restricted_field_id = self.services.config.CreateFieldDef(
7405 self.cnxn,
7406 self.project.project_id,
7407 'ResField',
7408 'STR_TYPE',
7409 None,
7410 None,
7411 None,
7412 None,
7413 None,
7414 None,
7415 None,
7416 None,
7417 None,
7418 None,
7419 None,
7420 None,
7421 None,
7422 None, [], [],
7423 is_restricted_field=True)
7424
7425 # Anon can only view fields in a public project.
7426 with self.work_env as we:
7427 anon_perms = we.ListFieldDefPermissions(field_id, self.project.project_id)
7428 self.assertEqual(anon_perms, [])
7429 with self.work_env as we:
7430 anon_perms = we.ListFieldDefPermissions(
7431 restricted_field_id, self.project.project_id)
7432 self.assertEqual(anon_perms, [])
7433
7434 # Anon cannot view fields in a private project.
7435 self.project.access = project_pb2.ProjectAccess.MEMBERS_ONLY
7436 with self.assertRaises(permissions.PermissionException):
7437 with self.work_env as we:
7438 anon_perms = we.ListFieldDefPermissions(
7439 field_id, self.project.project_id)
7440 with self.assertRaises(permissions.PermissionException):
7441 with self.work_env as we:
7442 anon_perms = we.ListFieldDefPermissions(
7443 restricted_field_id, self.project.project_id)
7444
7445 def testListFieldDefPermissions_SiteAdminAndProjectOwners(self):
7446 """SiteAdmins/ProjectOwners can always edit a field and its value."""
7447 field_id = self.services.config.CreateFieldDef(
7448 self.cnxn, self.project.project_id, 'Field', 'STR_TYPE', None, None,
7449 None, None, None, None, None, None, None, None, None, None, None, None,
7450 [], [])
7451 restricted_field_id = self.services.config.CreateFieldDef(
7452 self.cnxn,
7453 self.project.project_id,
7454 'ResField',
7455 'STR_TYPE',
7456 None,
7457 None,
7458 None,
7459 None,
7460 None,
7461 None,
7462 None,
7463 None,
7464 None,
7465 None,
7466 None,
7467 None,
7468 None,
7469 None, [], [],
7470 is_restricted_field=True)
7471
7472 self.SignIn(user_id=self.admin_user.user_id)
7473
7474 with self.work_env as we:
7475 site_admin_perms_1 = we.ListFieldDefPermissions(
7476 field_id, self.project.project_id)
7477 self.assertEqual(
7478 site_admin_perms_1,
7479 [permissions.EDIT_FIELD_DEF, permissions.EDIT_FIELD_DEF_VALUE])
7480
7481 with self.work_env as we:
7482 site_admin_perms_2 = we.ListFieldDefPermissions(
7483 restricted_field_id, self.project.project_id)
7484 self.assertEqual(
7485 site_admin_perms_2,
7486 [permissions.EDIT_FIELD_DEF, permissions.EDIT_FIELD_DEF_VALUE])
7487
7488 def testListFieldDefPermissions_FieldEditor(self):
7489 """Field Editors can edit the value of a field."""
7490 field_id = self.services.config.CreateFieldDef(
7491 self.cnxn, self.project.project_id, 'Field', 'STR_TYPE', None, None,
7492 None, None, None, None, None, None, None, None, None, None, None, None,
7493 [], [111])
7494 restricted_field_id = self.services.config.CreateFieldDef(
7495 self.cnxn,
7496 self.project.project_id,
7497 'ResField',
7498 'STR_TYPE',
7499 None,
7500 None,
7501 None,
7502 None,
7503 None,
7504 None,
7505 None,
7506 None,
7507 None,
7508 None,
7509 None,
7510 None,
7511 None,
7512 None, [], [111],
7513 is_restricted_field=True)
7514
7515 self.SignIn(user_id=self.user_1.user_id)
7516
7517 with self.work_env as we:
7518 field_editor_perms = we.ListFieldDefPermissions(
7519 field_id, self.project.project_id)
7520 self.assertEqual(field_editor_perms, [permissions.EDIT_FIELD_DEF_VALUE])
7521
7522 with self.work_env as we:
7523 field_editor_perms = we.ListFieldDefPermissions(
7524 restricted_field_id, self.project.project_id)
7525 self.assertEqual(field_editor_perms, [permissions.EDIT_FIELD_DEF_VALUE])
7526
7527
7528 # FUTURE: UpdateHotlist()
7529 # FUTURE: DeleteHotlist()