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