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