blob: c503fa06501fff15661bdc50fe1ba24db71005a4 [file] [log] [blame]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001# Copyright 2016 The Chromium Authors
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Copybara854996b2021-09-07 19:36:02 +00004
5"""Fake object classes that are useful for unit tests."""
6from __future__ import print_function
7from __future__ import division
8from __future__ import absolute_import
9
10import collections
11import itertools
12import logging
13import re
14import sys
15import time
16
17from six import string_types
18
19import settings
20from features import filterrules_helpers
21from framework import exceptions
22from framework import framework_bizobj
23from framework import framework_constants
24from framework import framework_helpers
25from framework import monorailrequest
26from framework import permissions
27from framework import profiler
28from framework import validate
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +010029from mrproto import features_pb2
30from mrproto import project_pb2
31from mrproto import tracker_pb2
32from mrproto import user_pb2
33from mrproto import usergroup_pb2
Copybara854996b2021-09-07 19:36:02 +000034from services import caches
35from services import config_svc
36from services import features_svc
37from services import project_svc
38from tracker import tracker_bizobj
39from tracker import tracker_constants
40
41# Many fakes return partial or constant values, regardless of their arguments.
42# pylint: disable=unused-argument
43
44BOUNDARY = '-----thisisaboundary'
45OWNER_ROLE = 'OWNER_ROLE'
46COMMITTER_ROLE = 'COMMITTER_ROLE'
47CONTRIBUTOR_ROLE = 'CONTRIBUTOR_ROLE'
48EDITOR_ROLE = 'EDITOR_ROLE'
49FOLLOWER_ROLE = 'FOLLOWER_ROLE'
50
51def Hotlist(
52 hotlist_name, hotlist_id, hotlist_item_fields=None,
53 is_private=False, owner_ids=None, editor_ids=None, follower_ids=None,
54 default_col_spec=None, summary=None, description=None):
55 hotlist_id = hotlist_id or hash(hotlist_name)
56 return features_pb2.MakeHotlist(
57 hotlist_name, hotlist_item_fields=hotlist_item_fields,
58 hotlist_id=hotlist_id, is_private=is_private, owner_ids=owner_ids or [],
59 editor_ids=editor_ids or [], follower_ids=follower_ids or [],
60 default_col_spec=default_col_spec, summary=summary,
61 description=description)
62
63def HotlistItem(issue_id, rank=None, adder_id=None, date_added=None, note=None):
64 return features_pb2.MakeHotlistItem(issue_id=issue_id, rank=rank,
65 adder_id=adder_id, date_added=date_added,
66 note=None)
67
68def Project(
69 project_name='proj', project_id=None, state=project_pb2.ProjectState.LIVE,
70 access=project_pb2.ProjectAccess.ANYONE, moved_to=None,
71 cached_content_timestamp=None,
72 owner_ids=None, committer_ids=None, contributor_ids=None):
73 """Returns a project protocol buffer with the given attributes."""
74 project_id = project_id or hash(project_name)
75 return project_pb2.MakeProject(
76 project_name, project_id=project_id, state=state, access=access,
77 moved_to=moved_to, cached_content_timestamp=cached_content_timestamp,
78 owner_ids=owner_ids, committer_ids=committer_ids,
79 contributor_ids=contributor_ids)
80
81
82def MakeTestFieldDef(
83 field_id, project_id, field_type, field_name='', applic_type=None,
84 applic_pred=None, is_required=False, is_niche=False, is_multivalued=False,
85 min_value=None, max_value=None, regex=None, needs_member=False,
86 needs_perm=None, grants_perm=None, notify_on=None, date_action_str=None,
87 docstring=None, admin_ids=None, editor_ids=None, approval_id=None,
88 is_phase_field=False, is_restricted_field=False):
89 return tracker_bizobj.MakeFieldDef(
90 field_id, project_id, field_name, field_type, applic_type, applic_pred,
91 is_required, is_niche, is_multivalued, min_value, max_value, regex,
92 needs_member, needs_perm, grants_perm, notify_on, date_action_str,
93 docstring, False,
94 approval_id=approval_id, is_phase_field=is_phase_field,
95 is_restricted_field=is_restricted_field, admin_ids=admin_ids,
96 editor_ids=editor_ids)
97
98def MakeTestApprovalDef(approval_id, approver_ids=None, survey=None):
99 return tracker_pb2.ApprovalDef(
100 approval_id=approval_id,
101 approver_ids = approver_ids,
102 survey = survey)
103
104def MakePhase(phase_id, name='', rank=0):
105 return tracker_pb2.Phase(phase_id=phase_id, name=name, rank=rank)
106
107
108def MakeApprovalValue(
109 approval_id,
110 status=tracker_pb2.ApprovalStatus.NOT_SET,
111 setter_id=None,
112 set_on=None,
113 approver_ids=None,
114 phase_id=None):
115 if approver_ids is None:
116 approver_ids = []
117 return tracker_pb2.ApprovalValue(
118 approval_id=approval_id,
119 status=status,
120 setter_id=setter_id,
121 set_on=set_on,
122 approver_ids=approver_ids,
123 phase_id=phase_id)
124
125
126def MakeFieldValue(
127 field_id,
128 int_value=None,
129 str_value=None,
130 user_id=None,
131 date_value=None,
132 url_value=None,
133 derived=None,
134 phase_id=None):
135 return tracker_pb2.FieldValue(
136 field_id=field_id,
137 int_value=int_value,
138 str_value=str_value,
139 user_id=user_id,
140 date_value=date_value,
141 url_value=url_value,
142 derived=derived,
143 phase_id=phase_id)
144
145
146def MakeTestIssue(
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100147 project_id,
148 local_id,
149 summary,
150 status,
151 owner_id,
152 labels=None,
153 derived_labels=None,
154 derived_status=None,
155 merged_into=0,
156 star_count=0,
157 derived_owner_id=0,
158 issue_id=None,
159 reporter_id=None,
160 opened_timestamp=None,
161 closed_timestamp=None,
162 modified_timestamp=None,
163 migration_modified_timestamp=None,
164 is_spam=False,
165 component_ids=None,
166 project_name=None,
167 field_values=None,
168 cc_ids=None,
169 derived_cc_ids=None,
170 assume_stale=True,
171 phases=None,
172 approval_values=None,
173 merged_into_external=None,
174 attachment_count=0,
175 derived_component_ids=None):
Copybara854996b2021-09-07 19:36:02 +0000176 """Easily make an Issue for testing."""
177 issue = tracker_pb2.Issue()
178 issue.project_id = project_id
179 issue.project_name = project_name
180 issue.local_id = local_id
181 issue.issue_id = issue_id if issue_id else 100000 + local_id
182 issue.reporter_id = reporter_id if reporter_id else owner_id
183 issue.summary = summary
184 issue.status = status
185 issue.owner_id = owner_id
186 issue.derived_owner_id = derived_owner_id
187 issue.star_count = star_count
188 issue.merged_into = merged_into
189 issue.merged_into_external = merged_into_external
190 issue.is_spam = is_spam
191 issue.attachment_count = attachment_count
192 if cc_ids:
193 issue.cc_ids = cc_ids
194 if derived_cc_ids:
195 issue.derived_cc_ids = derived_cc_ids
196 issue.assume_stale = assume_stale
197 if opened_timestamp:
198 issue.opened_timestamp = opened_timestamp
199 issue.owner_modified_timestamp = opened_timestamp
200 issue.status_modified_timestamp = opened_timestamp
201 issue.component_modified_timestamp = opened_timestamp
202 if modified_timestamp:
203 issue.modified_timestamp = modified_timestamp
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +0100204 # By default, make migration_modified_timestamp the same as
205 # modified_timestamp
206 issue.migration_modified_timestamp = modified_timestamp
207 if migration_modified_timestamp:
208 issue.migration_modified_timestamp = migration_modified_timestamp
Copybara854996b2021-09-07 19:36:02 +0000209 if closed_timestamp:
210 issue.closed_timestamp = closed_timestamp
211 if labels is not None:
212 if isinstance(labels, string_types):
213 labels = labels.split()
214 issue.labels.extend(labels)
215 if derived_labels is not None:
216 if isinstance(derived_labels, string_types):
217 derived_labels = derived_labels.split()
218 issue.derived_labels.extend(derived_labels)
219 if derived_status is not None:
220 issue.derived_status = derived_status
221 if component_ids is not None:
222 issue.component_ids = component_ids
223 if derived_component_ids is not None:
224 issue.derived_component_ids = derived_component_ids
225 if field_values is not None:
226 issue.field_values = field_values
227 if phases is not None:
228 issue.phases = phases
229 if approval_values is not None:
230 issue.approval_values = approval_values
231 return issue
232
233
234def MakeTestComponentDef(project_id, comp_id, path='', cc_ids=None):
235 if cc_ids is None:
236 cc_ids = []
237 return tracker_bizobj.MakeComponentDef(
238 comp_id, project_id, path, '', False, [], cc_ids, None, None)
239
240
241def MakeTestConfig(project_id, labels, statuses):
242 """Convenient function to make a ProjectIssueConfig object."""
243 config = tracker_bizobj.MakeDefaultProjectIssueConfig(project_id)
244 if isinstance(labels, string_types):
245 labels = labels.split()
246 if isinstance(statuses, string_types):
247 statuses = statuses.split()
248 config.well_known_labels = [
249 tracker_pb2.LabelDef(label=lab) for lab in labels]
250 config.well_known_statuses = [
251 tracker_pb2.StatusDef(status=stat) for stat in statuses]
252 return config
253
254
255class MonorailConnection(object):
256 """Fake connection to databases for use in tests."""
257
258 def Commit(self):
259 pass
260
261 def Close(self):
262 pass
263
264
265class MonorailRequest(monorailrequest.MonorailRequest):
266 """Subclass of MonorailRequest suitable for testing."""
267
268 def __init__(self, services, user_info=None, project=None, perms=None,
269 hotlist=None, **kwargs):
270 """Construct a test MonorailRequest.
271
272 Typically, this is constructed via testing.helpers.GetRequestObjects,
273 which also causes url parsing and optionally initializes the user,
274 project, and permissions info.
275
276 Args:
277 services: connections to backends.
278 user_info: a dict of user attributes to set on a MonorailRequest object.
279 For example, "user_id: 5" causes self.auth.user_id=5.
280 project: the Project pb for this request.
281 perms: a PermissionSet for this request.
282 """
283 super(MonorailRequest, self).__init__(services, **kwargs)
284
285 if user_info is not None:
286 for key in user_info:
287 setattr(self.auth, key, user_info[key])
288 if 'user_id' in user_info:
289 self.auth.effective_ids = {user_info['user_id']}
290
291 self.perms = perms or permissions.ADMIN_PERMISSIONSET
292 self.profiler = profiler.Profiler()
293 self.project = project
294 self.hotlist = hotlist
295 if hotlist is not None:
296 self.hotlist_id = hotlist.hotlist_id
297
298class UserGroupService(object):
299 """Fake UserGroupService class for testing other code."""
300
301 def __init__(self):
302 # Test-only sequence of expunged users.
303 self.expunged_users_in_groups = []
304
305 self.group_settings = {}
306 self.group_members = {}
307 self.group_addrs = {}
308 self.role_dict = {}
309
310 def TestAddGroupSettings(
311 self,
312 group_id,
313 email,
314 who_can_view=None,
315 anyone_can_join=False,
316 who_can_add=None,
317 external_group_type=None,
318 last_sync_time=0,
319 friend_projects=None,
320 notify_members=True,
321 notify_group=False):
322 """Set up a fake group for testing.
323
324 Args:
325 group_id: int user ID of the new user group.
326 email: string email address to identify the user group.
327 who_can_view: string enum 'owners', 'members', or 'anyone'.
328 anyone_can_join: optional boolean to allow any users to join the group.
329 who_can_add: optional list of int user IDs of users who can add
330 more members to the group.
331 notify_members: optional boolean for if emails to this group should be
332 sent directly to members.
333 notify_group: optional boolean for if emails to this group should be
334 sent directly to the group email.
335 """
336 friend_projects = friend_projects or []
337 group_settings = usergroup_pb2.MakeSettings(
338 who_can_view or 'members', external_group_type, last_sync_time,
339 friend_projects, notify_members, notify_group)
340 self.group_settings[group_id] = group_settings
341 self.group_addrs[group_id] = email
342 # TODO(jrobbins): store the other settings.
343
344 def TestAddMembers(self, group_id, user_ids, role='member'):
345 self.group_members.setdefault(group_id, []).extend(user_ids)
346 for user_id in user_ids:
347 self.role_dict.setdefault(group_id, {})[user_id] = role
348
349 def LookupAllMemberships(self, _cnxn, user_ids, use_cache=True):
350 return {
351 user_id: self.LookupMemberships(_cnxn, user_id)
352 for user_id in user_ids
353 }
354
355 def LookupMemberships(self, _cnxn, user_id):
356 memberships = {
357 group_id for group_id, member_ids in self.group_members.items()
358 if user_id in member_ids}
359 return memberships
360
361 def DetermineWhichUserIDsAreGroups(self, _cnxn, user_ids):
362 return [uid for uid in user_ids
363 if uid in self.group_settings]
364
365 def GetAllUserGroupsInfo(self, cnxn):
366 infos = []
367 for group_id in self.group_settings:
368 infos.append(
369 (self.group_addrs[group_id],
370 len(self.group_members.get(group_id, [])),
371 self.group_settings[group_id], group_id))
372
373 return infos
374
375 def GetAllGroupSettings(self, _cnxn, group_ids):
376 return {gid: self.group_settings[gid]
377 for gid in group_ids
378 if gid in self.group_settings}
379
380 def GetGroupSettings(self, cnxn, group_id):
381 return self.GetAllGroupSettings(cnxn, [group_id]).get(group_id)
382
383 def CreateGroup(self, cnxn, services, email, who_can_view_members,
384 ext_group_type=None, friend_projects=None):
385 friend_projects = friend_projects or []
386 group_id = services.user.LookupUserID(
387 cnxn, email, autocreate=True, allowgroups=True)
388 self.group_addrs[group_id] = email
389 group_settings = usergroup_pb2.MakeSettings(
390 who_can_view_members, ext_group_type, 0, friend_projects)
391 self.UpdateSettings(cnxn, group_id, group_settings)
392 return group_id
393
394 def DeleteGroups(self, cnxn, group_ids):
395 member_ids_dict, owner_ids_dict = self.LookupMembers(cnxn, group_ids)
396 citizens_id_dict = collections.defaultdict(list)
397 for g_id, user_ids in member_ids_dict.items():
398 citizens_id_dict[g_id].extend(user_ids)
399 for g_id, user_ids in owner_ids_dict.items():
400 citizens_id_dict[g_id].extend(user_ids)
401 for g_id, citizen_ids in citizens_id_dict.items():
402 # Remove group members, friend projects and settings
403 self.RemoveMembers(cnxn, g_id, citizen_ids)
404 self.group_settings.pop(g_id, None)
405
406 def LookupComputedMemberships(self, cnxn, domain, use_cache=True):
407 group_email = 'everyone@%s' % domain
408 group_id = self.LookupUserGroupID(cnxn, group_email, use_cache=use_cache)
409 if group_id:
410 return [group_id]
411
412 return []
413
414 def LookupUserGroupID(self, cnxn, group_email, use_cache=True):
415 for group_id in self.group_settings:
416 if group_email == self.group_addrs.get(group_id):
417 return group_id
418 return None
419
420 def LookupMembers(self, _cnxn, group_id_list):
421 members_dict = {}
422 owners_dict = {}
423 for gid in group_id_list:
424 members_dict[gid] = []
425 owners_dict[gid] = []
426 for mid in self.group_members.get(gid, []):
427 if self.role_dict.get(gid, {}).get(mid) == 'owner':
428 owners_dict[gid].append(mid)
429 elif self.role_dict.get(gid, {}).get(mid) == 'member':
430 members_dict[gid].append(mid)
431 return members_dict, owners_dict
432
433 def LookupAllMembers(self, _cnxn, group_id_list):
434 direct_members, direct_owners = self.LookupMembers(
435 _cnxn, group_id_list)
436 members_dict = {}
437 owners_dict = {}
438 for gid in group_id_list:
439 members = direct_members[gid]
440 owners = direct_owners[gid]
441 owners_dict[gid] = owners
442 members_dict[gid] = members
443 group_ids = set([uid for uid in members + owners
444 if uid in self.group_settings])
445 while group_ids:
446 indirect_members, indirect_owners = self.LookupMembers(
447 _cnxn, group_ids)
448 child_members = set()
449 child_owners = set()
450 for _, children in indirect_members.items():
451 child_members.update(children)
452 for _, children in indirect_owners.items():
453 child_owners.update(children)
454 members_dict[gid].extend(list(child_members))
455 owners_dict[gid].extend(list(child_owners))
456 group_ids = set(self.DetermineWhichUserIDsAreGroups(
457 _cnxn, list(child_members) + list(child_owners)))
458 members_dict[gid] = list(set(members_dict[gid]))
459 return members_dict, owners_dict
460
461
462 def RemoveMembers(self, _cnxn, group_id, old_member_ids):
463 current_member_ids = self.group_members.get(group_id, [])
464 revised_member_ids = [mid for mid in current_member_ids
465 if mid not in old_member_ids]
466 self.group_members[group_id] = revised_member_ids
467
468 def UpdateMembers(self, _cnxn, group_id, member_ids, new_role):
469 self.RemoveMembers(_cnxn, group_id, member_ids)
470 self.TestAddMembers(group_id, member_ids, new_role)
471
472 def UpdateSettings(self, _cnxn, group_id, group_settings):
473 self.group_settings[group_id] = group_settings
474
475 def ExpandAnyGroupEmailRecipients(self, cnxn, user_ids):
476 group_ids = set(self.DetermineWhichUserIDsAreGroups(cnxn, user_ids))
477 group_settings_dict = self.GetAllGroupSettings(cnxn, group_ids)
478 member_ids_dict, owner_ids_dict = self.LookupAllMembers(cnxn, group_ids)
479 indirect_ids = set()
480 direct_ids = {uid for uid in user_ids if uid not in group_ids}
481 for gid, group_settings in group_settings_dict.items():
482 if group_settings.notify_members:
483 indirect_ids.update(member_ids_dict.get(gid, set()))
484 indirect_ids.update(owner_ids_dict.get(gid, set()))
485 if group_settings.notify_group:
486 direct_ids.add(gid)
487
488 return list(direct_ids), list(indirect_ids)
489
490 def LookupVisibleMembers(
491 self, cnxn, group_id_list, perms, effective_ids, services):
492 settings_dict = self.GetAllGroupSettings(cnxn, group_id_list)
493 group_ids = list(settings_dict.keys())
494
495 direct_member_ids_dict, direct_owner_ids_dict = self.LookupMembers(
496 cnxn, group_ids)
497 all_member_ids_dict, all_owner_ids_dict = self.LookupAllMembers(
498 cnxn, group_ids)
499 visible_member_ids_dict = {}
500 visible_owner_ids_dict = {}
501 for gid in group_ids:
502 member_ids = all_member_ids_dict[gid]
503 owner_ids = all_owner_ids_dict[gid]
504 if permissions.CanViewGroupMembers(
505 perms, effective_ids, settings_dict[gid], member_ids, owner_ids, []):
506 visible_member_ids_dict[gid] = direct_member_ids_dict[gid]
507 visible_owner_ids_dict[gid] = direct_owner_ids_dict[gid]
508
509 return visible_member_ids_dict, visible_owner_ids_dict
510
511 def ValidateFriendProjects(self, cnxn, services, friend_projects):
512 project_names = list(filter(None, re.split('; |, | |;|,', friend_projects)))
513 id_dict = services.project.LookupProjectIDs(cnxn, project_names)
514 missed_projects = []
515 result = []
516 for p_name in project_names:
517 if p_name in id_dict:
518 result.append(id_dict[p_name])
519 else:
520 missed_projects.append(p_name)
521 error_msg = ''
522 if missed_projects:
523 error_msg = 'Project(s) %s do not exist' % ', '.join(missed_projects)
524 return None, error_msg
525 else:
526 return result, None
527
528 def ExpungeUsersInGroups(self, cnxn, ids):
529 self.expunged_users_in_groups.extend(ids)
530
531
532class CacheManager(object):
533
534 def __init__(self, invalidate_tbl=None):
535 self.last_call = None
536 self.cache_registry = collections.defaultdict(list)
537 self.processed_invalidations_up_to = 0
538
539 def RegisterCache(self, cache, kind):
540 """Register a cache to be notified of future invalidations."""
541 self.cache_registry[kind].append(cache)
542
543 def DoDistributedInvalidation(self, cnxn):
544 """Drop any cache entries that were invalidated by other jobs."""
545 self.last_call = 'DoDistributedInvalidation', cnxn
546
547 def StoreInvalidateRows(self, cnxn, kind, keys):
548 """Store database rows to let all frontends know to invalidate."""
549 self.last_call = 'StoreInvalidateRows', cnxn, kind, keys
550
551 def StoreInvalidateAll(self, cnxn, kind):
552 """Store a database row to let all frontends know to invalidate."""
553 self.last_call = 'StoreInvalidateAll', cnxn, kind
554
555
556
557class UserService(object):
558
559 def __init__(self):
560 """Creates a test-appropriate UserService object."""
561 self.users_by_email = {} # {email: user_id, ...}
562 self.users_by_id = {} # {user_id: email, ...}
563 self.test_users = {} # {user_id: user_pb, ...}
564 self.visited_hotlists = {} # user_id:[(hotlist_id, viewed), ...]
565 self.invite_rows = [] # (parent_id, child_id)
566 self.linked_account_rows = [] # (parent_id, child_id)
567 self.prefs_dict = {} # {user_id: UserPrefs}
568
569 def TestAddUser(
570 self, email, user_id, add_user=True, banned=False, obscure_email=True):
571 """Add a user to the fake UserService instance.
572
573 Args:
574 email: Email of the user.
575 user_id: int user ID.
576 add_user: Flag whether user pb should be created, i.e. whether a
577 Monorail account should be created
578 banned: Boolean to set the user as banned
579 obscure_email: Boolean to determine whether to obscure the user's email.
580
581 Returns:
582 The User PB that was added, or None.
583 """
584 self.users_by_email[email] = user_id
585 self.users_by_id[user_id] = email
586
587 user = None
588 if add_user:
589 user = user_pb2.MakeUser(user_id)
590 user.is_site_admin = False
591 user.email = email
592 user.obscure_email = obscure_email
593 if banned:
594 user.banned = 'is banned'
595 self.test_users[user_id] = user
596
597 return user
598
599 def GetUser(self, cnxn, user_id):
600 return self.GetUsersByIDs(cnxn, [user_id])[user_id]
601
602 def _CreateUser(self, _cnxn, email):
603 if email in self.users_by_email:
604 return
605 user_id = framework_helpers.MurmurHash3_x86_32(email)
606 self.TestAddUser(email, user_id)
607
608 def _CreateUsers(self, cnxn, emails):
609 for email in emails:
610 self._CreateUser(cnxn, email)
611
612 def LookupUserID(self, cnxn, email, autocreate=False, allowgroups=False):
613 email_dict = self.LookupUserIDs(
614 cnxn, [email], autocreate=autocreate, allowgroups=allowgroups)
615 if email in email_dict:
616 return email_dict[email]
617 raise exceptions.NoSuchUserException('%r not found' % email)
618
619 def GetUsersByIDs(self, cnxn, user_ids, use_cache=True, skip_missed=False):
620 user_dict = {}
621 for user_id in user_ids:
622 if user_id and self.test_users.get(user_id):
623 user_dict[user_id] = self.test_users[user_id]
624 elif not skip_missed:
625 user_dict[user_id] = user_pb2.MakeUser(user_id)
626 return user_dict
627
628 def LookupExistingUserIDs(self, cnxn, emails):
629 email_dict = {
630 email: self.users_by_email[email]
631 for email in emails
632 if email in self.users_by_email}
633 return email_dict
634
635 def LookupUserIDs(self, cnxn, emails, autocreate=False,
636 allowgroups=False):
637 email_dict = {}
638 needed_emails = [email.lower() for email in emails
639 if email
640 and not framework_constants.NO_VALUE_RE.match(email)]
641 for email in needed_emails:
642 user_id = self.users_by_email.get(email)
643 if not user_id:
644 if autocreate and validate.IsValidEmail(email):
645 self._CreateUser(cnxn, email)
646 user_id = self.users_by_email.get(email)
647 elif not autocreate:
648 raise exceptions.NoSuchUserException('%r' % email)
649 if user_id:
650 email_dict[email] = user_id
651 return email_dict
652
653 def LookupUserEmail(self, _cnxn, user_id):
654 email = self.users_by_id.get(user_id)
655 if not email:
656 raise exceptions.NoSuchUserException('No user has ID %r' % user_id)
657 return email
658
659 def LookupUserEmails(self, cnxn, user_ids, ignore_missed=False):
660 if ignore_missed:
661 user_dict = {}
662 for user_id in user_ids:
663 try:
664 user_dict[user_id] = self.LookupUserEmail(cnxn, user_id)
665 except exceptions.NoSuchUserException:
666 continue
667 return user_dict
668 user_dict = {
669 user_id: self.LookupUserEmail(cnxn, user_id)
670 for user_id in user_ids}
671 return user_dict
672
673 def UpdateUser(self, _cnxn, user_id, user):
674 """Updates the user pb."""
675 self.test_users[user_id] = user
676
677 def UpdateUserBan(self, _cnxn, user_id, user, is_banned=None,
678 banned_reason=None):
679 """Updates the user pb."""
680 self.test_users[user_id] = user
681 user.banned = banned_reason if is_banned else ''
682
683 def GetPendingLinkedInvites(self, cnxn, user_id):
684 invite_as_parent = [row[1] for row in self.invite_rows
685 if row[0] == user_id]
686 invite_as_child = [row[0] for row in self.invite_rows
687 if row[1] == user_id]
688 return invite_as_parent, invite_as_child
689
690 def InviteLinkedParent(self, cnxn, parent_id, child_id):
691 self.invite_rows.append((parent_id, child_id))
692
693 def AcceptLinkedChild(self, cnxn, parent_id, child_id):
694 if (parent_id, child_id) not in self.invite_rows:
695 raise exceptions.InputException('No such invite')
696 self.linked_account_rows.append((parent_id, child_id))
697 self.invite_rows = [
698 (p_id, c_id) for (p_id, c_id) in self.invite_rows
699 if p_id != parent_id and c_id != child_id]
700 self.GetUser(cnxn, parent_id).linked_child_ids.append(child_id)
701 self.GetUser(cnxn, child_id).linked_parent_id = parent_id
702
703 def UnlinkAccounts(self, _cnxn, parent_id, child_id):
704 """Delete a linked-account relationship."""
705 if not parent_id:
706 raise exceptions.InputException('Parent account is missing')
707 if not child_id:
708 raise exceptions.InputException('Child account is missing')
709 self.linked_account_rows = [(p, c) for (p, c) in self.linked_account_rows
710 if (p, c) != (parent_id, child_id)]
711
712 def UpdateUserSettings(
713 self, cnxn, user_id, user, notify=None, notify_starred=None,
714 email_compact_subject=None, email_view_widget=None,
715 notify_starred_ping=None, obscure_email=None, after_issue_update=None,
716 is_site_admin=None, is_banned=None, banned_reason=None,
717 keep_people_perms_open=None, preview_on_hover=None,
718 vacation_message=None):
719 # notifications
720 if notify is not None:
721 user.notify_issue_change = notify
722 if notify_starred is not None:
723 user.notify_starred_issue_change = notify_starred
724 if notify_starred_ping is not None:
725 user.notify_starred_ping = notify_starred_ping
726 if email_compact_subject is not None:
727 user.email_compact_subject = email_compact_subject
728 if email_view_widget is not None:
729 user.email_view_widget = email_view_widget
730
731 # display options
732 if after_issue_update is not None:
733 user.after_issue_update = user_pb2.IssueUpdateNav(after_issue_update)
734 if preview_on_hover is not None:
735 user.preview_on_hover = preview_on_hover
736 if keep_people_perms_open is not None:
737 user.keep_people_perms_open = keep_people_perms_open
738
739 # misc
740 if obscure_email is not None:
741 user.obscure_email = obscure_email
742
743 # admin
744 if is_site_admin is not None:
745 user.is_site_admin = is_site_admin
746 if is_banned is not None:
747 if is_banned:
748 user.banned = banned_reason or 'No reason given'
749 else:
750 user.reset('banned')
751
752 # user availability
753 if vacation_message is not None:
754 user.vacation_message = vacation_message
755
756 return self.UpdateUser(cnxn, user_id, user)
757
758 def GetUsersPrefs(self, cnxn, user_ids, use_cache=True):
759 for user_id in user_ids:
760 if user_id not in self.prefs_dict:
761 self.prefs_dict[user_id] = user_pb2.UserPrefs(user_id=user_id)
762 return self.prefs_dict
763
764 def GetUserPrefs(self, cnxn, user_id, use_cache=True):
765 """Return a UserPrefs PB for the requested user ID."""
766 prefs_dict = self.GetUsersPrefs(cnxn, [user_id], use_cache=use_cache)
767 return prefs_dict[user_id]
768
769 def GetUserPrefsByEmail(self, cnxn, email, use_cache=True):
770 """Return a UserPrefs PB for the requested email, or an empty UserPrefs."""
771 try:
772 user_id = self.LookupUserID(cnxn, email)
773 user_prefs = self.GetUserPrefs(cnxn, user_id, use_cache=use_cache)
774 except exceptions.NoSuchUserException:
775 user_prefs = user_pb2.UserPrefs()
776 return user_prefs
777
778 def SetUserPrefs(self, cnxn, user_id, pref_values):
779 userprefs = self.GetUserPrefs(cnxn, user_id)
780 names_to_overwrite = {upv.name for upv in pref_values}
781 userprefs.prefs = [upv for upv in userprefs.prefs
782 if upv.name not in names_to_overwrite]
783 userprefs.prefs.extend(pref_values)
784
785 def ExpungeUsers(self, cnxn, user_ids):
786 for user_id in user_ids:
787 self.test_users.pop(user_id, None)
788 self.prefs_dict.pop(user_id, None)
789 email = self.users_by_id.pop(user_id, None)
790 if email:
791 self.users_by_email.pop(email, None)
792
793 self.invite_rows = [row for row in self.invite_rows
794 if row[0] not in user_ids and row[1] not in user_ids]
795 self.linked_account_rows = [
796 row for row in self.linked_account_rows
797 if row[0] not in user_ids and row[1] not in user_ids]
798
799 def TotalUsersCount(self, cnxn):
800 return len(self.users_by_id) - 1 if (
801 framework_constants.DELETED_USER_ID in self.users_by_id
802 ) else len(self.users_by_id)
803
804 def GetAllUserEmailsBatch(self, cnxn, limit=1000, offset=0):
805 sorted_user_ids = sorted(self.users_by_id.keys())
806 sorted_user_ids = [
807 user_id for user_id in sorted_user_ids
808 if user_id != framework_constants.DELETED_USER_ID]
809 emails = []
810 for i in range(offset, offset + limit):
811 try:
812 user_id = sorted_user_ids[i]
813 if user_id != framework_constants.DELETED_USER_ID:
814 emails.append(self.users_by_id[user_id])
815 except IndexError:
816 break
817 return emails
818
819 def GetRecentlyVisitedHotlists(self, _cnxn, user_id):
820 try:
821 return self.visited_hotlists[user_id]
822 except KeyError:
823 return []
824
825 def AddVisitedHotlist(self, _cnxn, user_id, hotlist_id, commit=True):
826 try:
827 user_visited_tuples = self.visited_hotlists[user_id]
828 self.visited_hotlists[user_id] = [
829 hid for hid in user_visited_tuples if hid != hotlist_id]
830 except KeyError:
831 self.visited_hotlists[user_id] = []
832 self.visited_hotlists[user_id].append(hotlist_id)
833
834 def ExpungeUsersHotlistsHistory(self, cnxn, user_ids, commit=True):
835 for user_id in user_ids:
836 self.visited_hotlists.pop(user_id, None)
837
838
839class AbstractStarService(object):
840 """Fake StarService."""
841
842 def __init__(self):
843 self.stars_by_item_id = {}
844 self.stars_by_starrer_id = {}
845 self.expunged_item_ids = []
846
847 def ExpungeStars(self, _cnxn, item_id, commit=True, limit=None):
848 self.expunged_item_ids.append(item_id)
849 old_starrers = self.stars_by_item_id.get(item_id, [])
850 self.stars_by_item_id[item_id] = []
851 for old_starrer in old_starrers:
852 if self.stars_by_starrer_id.get(old_starrer):
853 self.stars_by_starrer_id[old_starrer] = [
854 it for it in self.stars_by_starrer_id[old_starrer]
855 if it != item_id]
856
857 def ExpungeStarsByUsers(self, _cnxn, user_ids, limit=None):
858 for user_id in user_ids:
859 item_ids = self.stars_by_starrer_id.pop(user_id, [])
860 for item_id in item_ids:
861 starrers = self.stars_by_item_id.get(item_id, None)
862 if starrers:
863 self.stars_by_item_id[item_id] = [
864 starrer for starrer in starrers if starrer != user_id]
865
866 def LookupItemStarrers(self, _cnxn, item_id):
867 return self.stars_by_item_id.get(item_id, [])
868
869 def LookupItemsStarrers(self, cnxn, item_ids):
870 return {
871 item_id: self.LookupItemStarrers(cnxn, item_id) for item_id in item_ids}
872
873 def LookupStarredItemIDs(self, _cnxn, starrer_user_id):
874 return self.stars_by_starrer_id.get(starrer_user_id, [])
875
876 def IsItemStarredBy(self, cnxn, item_id, starrer_user_id):
877 return item_id in self.LookupStarredItemIDs(cnxn, starrer_user_id)
878
879 def CountItemStars(self, cnxn, item_id):
880 return len(self.LookupItemStarrers(cnxn, item_id))
881
882 def CountItemsStars(self, cnxn, item_ids):
883 return {item_id: self.CountItemStars(cnxn, item_id)
884 for item_id in item_ids}
885
886 def _SetStar(self, cnxn, item_id, starrer_user_id, starred):
887 if starred and not self.IsItemStarredBy(cnxn, item_id, starrer_user_id):
888 self.stars_by_item_id.setdefault(item_id, []).append(starrer_user_id)
889 self.stars_by_starrer_id.setdefault(starrer_user_id, []).append(item_id)
890
891 elif not starred and self.IsItemStarredBy(cnxn, item_id, starrer_user_id):
892 self.stars_by_item_id[item_id].remove(starrer_user_id)
893 self.stars_by_starrer_id[starrer_user_id].remove(item_id)
894
895 def SetStar(self, cnxn, item_id, starrer_user_id, starred):
896 self._SetStar(cnxn, item_id, starrer_user_id, starred)
897
898 def SetStarsBatch(
899 self, cnxn, item_id, starrer_user_ids, starred, commit=True):
900 for starrer_user_id in starrer_user_ids:
901 self._SetStar(cnxn, item_id, starrer_user_id, starred)
902
903
904class UserStarService(AbstractStarService):
905 pass
906
907
908class ProjectStarService(AbstractStarService):
909 pass
910
911
912class HotlistStarService(AbstractStarService):
913 pass
914
915
916class IssueStarService(AbstractStarService):
917
918 # pylint: disable=arguments-differ
919 def SetStar(
920 self, cnxn, services, _config, issue_id, starrer_user_id,
921 starred):
922 super(IssueStarService, self).SetStar(
923 cnxn, issue_id, starrer_user_id, starred)
924 try:
925 issue = services.issue.GetIssue(cnxn, issue_id)
926 issue.star_count += (1 if starred else -1)
927 except exceptions.NoSuchIssueException:
928 pass
929
930 # pylint: disable=arguments-differ
931 def SetStarsBatch(
932 self, cnxn, _service, _config, issue_id, starrer_user_ids,
933 starred):
934 super(IssueStarService, self).SetStarsBatch(
935 cnxn, issue_id, starrer_user_ids, starred)
936
937 def SetStarsBatch_SkipIssueUpdate(
938 self, cnxn, issue_id, starrer_user_ids, starred, commit=True):
939 super(IssueStarService, self).SetStarsBatch(
940 cnxn, issue_id, starrer_user_ids, starred)
941
942
943class ProjectService(object):
944 """Fake ProjectService object.
945
946 Provides methods for creating users and projects, which are accessible
947 through parts of the real ProjectService interface.
948 """
949
950 def __init__(self):
951 self.test_projects = {} # project_name -> project_pb
952 self.projects_by_id = {} # project_id -> project_pb
953 self.test_star_manager = None
954 self.indexed_projects = {}
955 self.unindexed_projects = set()
956 self.index_counter = 0
957 self.project_commitments = {}
958 self.ac_exclusion_ids = {}
959 self.no_expand_ids = {}
960
961 def TestAddProject(
962 self, name, summary='', state=project_pb2.ProjectState.LIVE,
963 owner_ids=None, committer_ids=None, contrib_ids=None,
964 issue_notify_address=None, state_reason='', description=None,
965 project_id=None, process_inbound_email=None, access=None,
966 extra_perms=None):
967 """Add a project to the fake ProjectService object.
968
969 Args:
970 name: The name of the project. Will replace any existing project under
971 the same name.
972 summary: The summary string of the project.
973 state: Initial state for the project from project_pb2.ProjectState.
974 owner_ids: List of user ids for project owners
975 committer_ids: List of user ids for project committers
976 contrib_ids: List of user ids for project contributors
977 issue_notify_address: email address to send issue change notifications
978 state_reason: string describing the reason the project is in its current
979 state.
980 description: The description string for this project
981 project_id: A unique integer identifier for the created project.
982 process_inbound_email: True to make this project accept inbound email.
983 access: One of the values of enum project_pb2.ProjectAccess.
984 extra_perms: List of ExtraPerms PBs for project members.
985
986 Returns:
987 A populated project PB.
988 """
989 proj_pb = project_pb2.Project()
990 proj_pb.project_id = project_id or hash(name) % 100000
991 proj_pb.project_name = name
992 proj_pb.summary = summary
993 proj_pb.state = state
994 proj_pb.state_reason = state_reason
995 proj_pb.extra_perms = extra_perms or []
996 if description is not None:
997 proj_pb.description = description
998
999 self.TestAddProjectMembers(owner_ids, proj_pb, OWNER_ROLE)
1000 self.TestAddProjectMembers(committer_ids, proj_pb, COMMITTER_ROLE)
1001 self.TestAddProjectMembers(contrib_ids, proj_pb, CONTRIBUTOR_ROLE)
1002
1003 if issue_notify_address is not None:
1004 proj_pb.issue_notify_address = issue_notify_address
1005 if process_inbound_email is not None:
1006 proj_pb.process_inbound_email = process_inbound_email
1007 if access is not None:
1008 proj_pb.access = access
1009
1010 self.test_projects[name] = proj_pb
1011 self.projects_by_id[proj_pb.project_id] = proj_pb
1012 return proj_pb
1013
1014 def TestAddProjectMembers(self, user_id_list, proj_pb, role):
1015 if user_id_list is not None:
1016 for user_id in user_id_list:
1017 if role == OWNER_ROLE:
1018 proj_pb.owner_ids.append(user_id)
1019 elif role == COMMITTER_ROLE:
1020 proj_pb.committer_ids.append(user_id)
1021 elif role == CONTRIBUTOR_ROLE:
1022 proj_pb.contributor_ids.append(user_id)
1023
1024 def LookupProjectIDs(self, cnxn, project_names):
1025 return {
1026 project_name: self.test_projects[project_name].project_id
1027 for project_name in project_names
1028 if project_name in self.test_projects}
1029
1030 def LookupProjectNames(self, cnxn, project_ids):
1031 projects_dict = self.GetProjects(cnxn, project_ids)
1032 return {p.project_id: p.project_name
1033 for p in projects_dict.values()}
1034
1035 def CreateProject(
1036 self, _cnxn, project_name, owner_ids, committer_ids,
1037 contributor_ids, summary, description,
1038 state=project_pb2.ProjectState.LIVE, access=None,
1039 read_only_reason=None,
1040 home_page=None, docs_url=None, source_url=None,
1041 logo_gcs_id=None, logo_file_name=None):
1042 """Create and store a Project with the given attributes."""
1043 if project_name in self.test_projects:
1044 raise exceptions.ProjectAlreadyExists()
1045 project = self.TestAddProject(
1046 project_name, summary=summary, state=state,
1047 owner_ids=owner_ids, committer_ids=committer_ids,
1048 contrib_ids=contributor_ids, description=description,
1049 access=access)
1050 return project.project_id
1051
1052 def ExpungeProject(self, _cnxn, project_id):
1053 project = self.projects_by_id.get(project_id)
1054 if project:
1055 self.test_projects.pop(project.project_name, None)
1056
1057 def GetProjectsByName(self, _cnxn, project_name_list, use_cache=True):
1058 return {
1059 pn: self.test_projects[pn] for pn in project_name_list
1060 if pn in self.test_projects}
1061
1062 def GetProjectByName(self, _cnxn, name, use_cache=True):
1063 return self.test_projects.get(name)
1064
1065 def GetProjectList(self, cnxn, project_id_list, use_cache=True):
1066 project_dict = self.GetProjects(cnxn, project_id_list, use_cache=use_cache)
1067 return [project_dict[pid] for pid in project_id_list
1068 if pid in project_dict]
1069
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001070 def GetVisibleProjects(
Copybara854996b2021-09-07 19:36:02 +00001071 self, _cnxn, logged_in_user, effective_ids, domain=None, use_cache=True):
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001072 project_ids = sorted(self.projects_by_id.keys())
Copybara854996b2021-09-07 19:36:02 +00001073 visible_project_ids = []
1074 for pid in project_ids:
1075 can_view = permissions.UserCanViewProject(
1076 logged_in_user, effective_ids, self.projects_by_id[pid])
1077 different_domain = framework_helpers.GetNeededDomain(
1078 self.projects_by_id[pid].project_name, domain)
1079 if can_view and not different_domain:
1080 visible_project_ids.append(pid)
1081
1082 return visible_project_ids
1083
1084 def GetProjects(self, _cnxn, project_ids, use_cache=True):
1085 result = {}
1086 for project_id in project_ids:
1087 project = self.projects_by_id.get(project_id)
1088 if project:
1089 result[project_id] = project
1090 else:
1091 raise exceptions.NoSuchProjectException(project_id)
1092 return result
1093
1094 def GetAllProjects(self, _cnxn, use_cache=True):
1095 result = {}
1096 for project_id in self.projects_by_id:
1097 project = self.projects_by_id.get(project_id)
1098 result[project_id] = project
1099 return result
1100
1101
1102 def GetProject(self, cnxn, project_id, use_cache=True):
1103 """Load the specified project from the database."""
1104 project_id_dict = self.GetProjects(cnxn, [project_id], use_cache=use_cache)
1105 if project_id not in project_id_dict:
1106 raise exceptions.NoSuchProjectException()
1107 return project_id_dict[project_id]
1108
1109 def GetProjectCommitments(self, _cnxn, project_id):
1110 if project_id in self.project_commitments:
1111 return self.project_commitments[project_id]
1112
1113 project_commitments = project_pb2.ProjectCommitments()
1114 project_commitments.project_id = project_id
1115 return project_commitments
1116
1117 def TestStoreProjectCommitments(self, project_commitments):
1118 key = project_commitments.project_id
1119 self.project_commitments[key] = project_commitments
1120
1121 def GetProjectAutocompleteExclusion(self, cnxn, project_id):
1122 return (self.ac_exclusion_ids.get(project_id, []),
1123 self.no_expand_ids.get(project_id, []))
1124
1125 def UpdateProject(
1126 self,
1127 _cnxn,
1128 project_id,
1129 summary=None,
1130 description=None,
1131 state=None,
1132 state_reason=None,
1133 access=None,
1134 issue_notify_address=None,
1135 attachment_bytes_used=None,
1136 attachment_quota=None,
1137 moved_to=None,
1138 process_inbound_email=None,
1139 only_owners_remove_restrictions=None,
1140 read_only_reason=None,
1141 cached_content_timestamp=None,
1142 only_owners_see_contributors=None,
1143 delete_time=None,
1144 recent_activity=None,
1145 revision_url_format=None,
1146 home_page=None,
1147 docs_url=None,
1148 source_url=None,
1149 logo_gcs_id=None,
1150 logo_file_name=None,
1151 issue_notify_always_detailed=None,
1152 commit=True):
1153 project = self.projects_by_id.get(project_id)
1154 if not project:
1155 raise exceptions.NoSuchProjectException(
1156 'Project "%s" not found!' % project_id)
1157
1158 # TODO(jrobbins): implement all passed arguments - probably as a utility
1159 # method shared with the real persistence implementation.
1160 if read_only_reason is not None:
1161 project.read_only_reason = read_only_reason
1162 if attachment_bytes_used is not None:
1163 project.attachment_bytes_used = attachment_bytes_used
1164
1165 def UpdateProjectRoles(
1166 self, _cnxn, project_id, owner_ids, committer_ids,
1167 contributor_ids, now=None):
1168 project = self.projects_by_id.get(project_id)
1169 if not project:
1170 raise exceptions.NoSuchProjectException(
1171 'Project "%s" not found!' % project_id)
1172
1173 project.owner_ids = owner_ids
1174 project.committer_ids = committer_ids
1175 project.contributor_ids = contributor_ids
1176
1177 def MarkProjectDeletable(
1178 self, _cnxn, project_id, _config_service):
1179 project = self.projects_by_id[project_id]
1180 project.project_name = 'DELETABLE_%d' % project_id
1181 project.state = project_pb2.ProjectState.DELETABLE
1182
1183 def UpdateRecentActivity(self, _cnxn, _project_id, now=None):
1184 pass
1185
1186 def GetUserRolesInAllProjects(self, _cnxn, effective_ids):
1187 owned_project_ids = set()
1188 membered_project_ids = set()
1189 contrib_project_ids = set()
1190
1191 for project in self.projects_by_id.values():
1192 if not effective_ids.isdisjoint(project.owner_ids):
1193 owned_project_ids.add(project.project_id)
1194 elif not effective_ids.isdisjoint(project.committer_ids):
1195 membered_project_ids.add(project.project_id)
1196 elif not effective_ids.isdisjoint(project.contributor_ids):
1197 contrib_project_ids.add(project.project_id)
1198
1199 return owned_project_ids, membered_project_ids, contrib_project_ids
1200
1201 def GetProjectMemberships(self, _cnxn, effective_ids, use_cache=True):
1202 # type: MonorailConnection, Collection[int], bool ->
1203 # Mapping[int, Collection[int]]
1204 projects_by_user_id = collections.defaultdict(set)
1205
1206 for project in self.projects_by_id.values():
1207 member_ids = set(
1208 itertools.chain(
1209 project.owner_ids, project.committer_ids,
1210 project.contributor_ids))
1211 for user_id in effective_ids:
1212 if user_id in member_ids:
1213 projects_by_user_id[user_id].add(project.project_id)
1214 return projects_by_user_id
1215
1216 def ExpungeUsersInProjects(self, cnxn, user_ids, limit=None):
1217 for project in self.projects_by_id.values():
1218 project.owner_ids = [owner_id for owner_id in project.owner_ids
1219 if owner_id not in user_ids]
1220 project.committer_ids = [com_id for com_id in project.committer_ids
1221 if com_id not in user_ids]
1222 project.contributor_ids = [con_id for con_id in project.contributor_ids
1223 if con_id not in user_ids]
1224
1225
1226class ConfigService(object):
1227 """Fake version of ConfigService that just works in-RAM."""
1228
1229 def __init__(self, user_id=None):
1230 self.project_configs = {}
1231 self.next_field_id = 123
1232 self.next_component_id = 345
1233 self.next_template_id = 23
1234 self.expunged_configs = []
1235 self.expunged_users_in_configs = []
1236 self.component_ids_to_templates = {}
1237 self.label_to_id = {}
1238 self.id_to_label = {}
1239 self.strict = False # Set true to raise more exceptions like real class.
1240
1241 def TestAddLabelsDict(self, label_to_id):
1242 self.label_to_id = label_to_id
1243 self.id_to_label = {
1244 label_id: label
1245 for label, label_id in list(self.label_to_id.items())}
1246
1247 def TestAddFieldDef(self, fd):
1248 self.project_configs[fd.project_id].field_defs.append(fd)
1249
1250 def TestAddApprovalDef(self, ad, project_id):
1251 self.project_configs[project_id].approval_defs.append(ad)
1252
1253 def ExpungeConfig(self, _cnxn, project_id):
1254 self.expunged_configs.append(project_id)
1255
1256 def ExpungeUsersInConfigs(self, _cnxn, user_ids, limit=None):
1257 self.expunged_users_in_configs.extend(user_ids)
1258
1259 def GetLabelDefRows(self, cnxn, project_id, use_cache=True):
1260 """This always returns empty results. Mock it to test other cases."""
1261 return []
1262
1263 def GetLabelDefRowsAnyProject(self, cnxn, where=None):
1264 """This always returns empty results. Mock it to test other cases."""
1265 return []
1266
1267 def LookupLabel(self, cnxn, project_id, label_id):
1268 if label_id in self.id_to_label:
1269 return self.id_to_label[label_id]
1270 if label_id == 999:
1271 return None
1272 return 'label_%d_%d' % (project_id, label_id)
1273
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001274 def LookupLabelID(
1275 self, cnxn, project_id, label, autocreate=True, case_sensitive=False):
Copybara854996b2021-09-07 19:36:02 +00001276 if label in self.label_to_id:
1277 return self.label_to_id[label]
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001278 # TODO: The condition here is specifically added to return 'None' and
1279 # allow testing for label freezing. This can be removed after refactoring
1280 # other dependent tests to not fail for returning 'None' instead of '1'
1281 # when label is not found in 'label_to_id' dict.
1282 if label == 'freeze_new_label':
1283 return None
Copybara854996b2021-09-07 19:36:02 +00001284 return 1
1285
1286 def LookupLabelIDs(self, cnxn, project_id, labels, autocreate=False):
1287 ids = []
1288 next_label_id = 0
1289 if self.id_to_label.keys():
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001290 existing_ids = sorted(self.id_to_label.keys())
Copybara854996b2021-09-07 19:36:02 +00001291 next_label_id = existing_ids[-1] + 1
1292 for label in labels:
1293 if self.label_to_id.get(label) is not None:
1294 ids.append(self.label_to_id[label])
1295 elif autocreate:
1296 self.label_to_id[label] = next_label_id
1297 self.id_to_label[next_label_id] = label
1298 ids.append(next_label_id)
1299 next_label_id += 1
1300 return ids
1301
1302 def LookupIDsOfLabelsMatching(self, cnxn, project_id, regex):
1303 return [1, 2, 3]
1304
1305 def LookupStatus(self, cnxn, project_id, status_id):
1306 return 'status_%d_%d' % (project_id, status_id)
1307
1308 def LookupStatusID(self, cnxn, project_id, status, autocreate=True):
1309 if status:
1310 return 1
1311 else:
1312 return 0
1313
1314 def LookupStatusIDs(self, cnxn, project_id, statuses):
1315 return [idx for idx, _status in enumerate(statuses)]
1316
1317 def LookupClosedStatusIDs(self, cnxn, project_id):
1318 return [7, 8, 9]
1319
1320 def StoreConfig(self, _cnxn, config):
1321 self.project_configs[config.project_id] = config
1322
1323 def GetProjectConfig(self, _cnxn, project_id, use_cache=True):
1324 if project_id in self.project_configs:
1325 return self.project_configs[project_id]
1326 elif self.strict:
1327 raise exceptions.NoSuchProjectException()
1328 else:
1329 return tracker_bizobj.MakeDefaultProjectIssueConfig(project_id)
1330
1331 def GetProjectConfigs(self, _cnxn, project_ids, use_cache=True):
1332 config_dict = {}
1333 for project_id in project_ids:
1334 if project_id in self.project_configs:
1335 config_dict[project_id] = self.project_configs[project_id]
1336 elif not self.strict:
1337 config_dict[project_id] = tracker_bizobj.MakeDefaultProjectIssueConfig(
1338 project_id)
1339 return config_dict
1340
1341 def UpdateConfig(
1342 self, cnxn, project, well_known_statuses=None,
1343 statuses_offer_merge=None, well_known_labels=None,
1344 excl_label_prefixes=None, default_template_for_developers=None,
1345 default_template_for_users=None, list_prefs=None, restrict_to_known=None,
1346 approval_defs=None):
1347 project_id = project.project_id
1348 project_config = self.GetProjectConfig(cnxn, project_id, use_cache=False)
1349
1350 if well_known_statuses is not None:
1351 tracker_bizobj.SetConfigStatuses(project_config, well_known_statuses)
1352
1353 if statuses_offer_merge is not None:
1354 project_config.statuses_offer_merge = statuses_offer_merge
1355
1356 if well_known_labels is not None:
1357 tracker_bizobj.SetConfigLabels(project_config, well_known_labels)
1358
1359 if excl_label_prefixes is not None:
1360 project_config.exclusive_label_prefixes = excl_label_prefixes
1361
1362 if approval_defs is not None:
1363 tracker_bizobj.SetConfigApprovals(project_config, approval_defs)
1364
1365 if default_template_for_developers is not None:
1366 project_config.default_template_for_developers = (
1367 default_template_for_developers)
1368 if default_template_for_users is not None:
1369 project_config.default_template_for_users = default_template_for_users
1370
1371 if list_prefs:
1372 default_col_spec, default_sort_spec, x_attr, y_attr, m_d_q = list_prefs
1373 project_config.default_col_spec = default_col_spec
1374 project_config.default_sort_spec = default_sort_spec
1375 project_config.default_x_attr = x_attr
1376 project_config.default_y_attr = y_attr
1377 project_config.member_default_query = m_d_q
1378
1379 if restrict_to_known is not None:
1380 project_config.restrict_to_known = restrict_to_known
1381
1382 self.StoreConfig(cnxn, project_config)
1383 return project_config
1384
1385 def CreateFieldDef(
1386 self,
1387 cnxn,
1388 project_id,
1389 field_name,
1390 field_type_str,
1391 applic_type,
1392 applic_pred,
1393 is_required,
1394 is_niche,
1395 is_multivalued,
1396 min_value,
1397 max_value,
1398 regex,
1399 needs_member,
1400 needs_perm,
1401 grants_perm,
1402 notify_on,
1403 date_action_str,
1404 docstring,
1405 admin_ids,
1406 editor_ids,
1407 approval_id=None,
1408 is_phase_field=False,
1409 is_restricted_field=False):
1410 config = self.GetProjectConfig(cnxn, project_id)
1411 field_type = tracker_pb2.FieldTypes(field_type_str)
1412 field_id = self.next_field_id
1413 self.next_field_id += 1
1414 fd = tracker_bizobj.MakeFieldDef(
1415 field_id, project_id, field_name, field_type, applic_type, applic_pred,
1416 is_required, is_niche, is_multivalued, min_value, max_value, regex,
1417 needs_member, needs_perm, grants_perm, notify_on, date_action_str,
1418 docstring, False, approval_id, is_phase_field, is_restricted_field,
1419 admin_ids=admin_ids, editor_ids=editor_ids)
1420 config.field_defs.append(fd)
1421 self.StoreConfig(cnxn, config)
1422 return field_id
1423
1424 def LookupFieldID(self, cnxn, project_id, field):
1425 config = self.GetProjectConfig(cnxn, project_id)
1426 for fd in config.field_defs:
1427 if fd.field_name == field:
1428 return fd.field_id
1429
1430 return None
1431
1432 def SoftDeleteFieldDefs(self, cnxn, project_id, field_ids):
1433 config = self.GetProjectConfig(cnxn, project_id)
1434 for fd in config.field_defs:
1435 if fd.field_id in field_ids:
1436 fd.is_deleted = True
1437 self.StoreConfig(cnxn, config)
1438
1439 def UpdateFieldDef(
1440 self,
1441 cnxn,
1442 project_id,
1443 field_id,
1444 field_name=None,
1445 applicable_type=None,
1446 applicable_predicate=None,
1447 is_required=None,
1448 is_niche=None,
1449 is_multivalued=None,
1450 min_value=None,
1451 max_value=None,
1452 regex=None,
1453 needs_member=None,
1454 needs_perm=None,
1455 grants_perm=None,
1456 notify_on=None,
1457 date_action=None,
1458 docstring=None,
1459 admin_ids=None,
1460 editor_ids=None,
1461 is_restricted_field=None):
1462 config = self.GetProjectConfig(cnxn, project_id)
1463 fd = tracker_bizobj.FindFieldDefByID(field_id, config)
1464 # pylint: disable=multiple-statements
1465 if field_name is not None: fd.field_name = field_name
1466 if applicable_type is not None: fd.applicable_type = applicable_type
1467 if applicable_predicate is not None:
1468 fd.applicable_predicate = applicable_predicate
1469 if is_required is not None: fd.is_required = is_required
1470 if is_niche is not None: fd.is_niche = is_niche
1471 if is_multivalued is not None: fd.is_multivalued = is_multivalued
1472 if min_value is not None: fd.min_value = min_value
1473 if max_value is not None: fd.max_value = max_value
1474 if regex is not None: fd.regex = regex
1475 if date_action is not None:
1476 fd.date_action = config_svc.DATE_ACTION_ENUM.index(date_action)
1477 if docstring is not None: fd.docstring = docstring
1478 if admin_ids is not None: fd.admin_ids = admin_ids
1479 if editor_ids is not None:
1480 fd.editor_ids = editor_ids
1481 if is_restricted_field is not None:
1482 fd.is_restricted_field = is_restricted_field
1483 self.StoreConfig(cnxn, config)
1484
1485 def CreateComponentDef(
1486 self, cnxn, project_id, path, docstring, deprecated, admin_ids, cc_ids,
1487 created, creator_id, label_ids):
1488 config = self.GetProjectConfig(cnxn, project_id)
1489 cd = tracker_bizobj.MakeComponentDef(
1490 self.next_component_id, project_id, path, docstring, deprecated,
1491 admin_ids, cc_ids, created, creator_id, label_ids=label_ids)
1492 config.component_defs.append(cd)
1493 self.next_component_id += 1
1494 self.StoreConfig(cnxn, config)
1495 return self.next_component_id - 1
1496
1497 def UpdateComponentDef(
1498 self, cnxn, project_id, component_id, path=None, docstring=None,
1499 deprecated=None, admin_ids=None, cc_ids=None, created=None,
1500 creator_id=None, modified=None, modifier_id=None, label_ids=None):
1501 config = self.GetProjectConfig(cnxn, project_id)
1502 cd = tracker_bizobj.FindComponentDefByID(component_id, config)
1503 if path is not None:
1504 assert path
1505 cd.path = path
1506 # pylint: disable=multiple-statements
1507 if docstring is not None: cd.docstring = docstring
1508 if deprecated is not None: cd.deprecated = deprecated
1509 if admin_ids is not None: cd.admin_ids = admin_ids
1510 if cc_ids is not None: cd.cc_ids = cc_ids
1511 if created is not None: cd.created = created
1512 if creator_id is not None: cd.creator_id = creator_id
1513 if modified is not None: cd.modified = modified
1514 if modifier_id is not None: cd.modifier_id = modifier_id
1515 if label_ids is not None: cd.label_ids = label_ids
1516 self.StoreConfig(cnxn, config)
1517
1518 def DeleteComponentDef(self, cnxn, project_id, component_id):
1519 """Delete the specified component definition."""
1520 config = self.GetProjectConfig(cnxn, project_id)
1521 config.component_defs = [
1522 cd for cd in config.component_defs
1523 if cd.component_id != component_id]
1524 self.StoreConfig(cnxn, config)
1525
1526 def InvalidateMemcache(self, issues, key_prefix=''):
1527 pass
1528
1529 def InvalidateMemcacheForEntireProject(self, project_id):
1530 pass
1531
1532
1533class IssueService(object):
1534 """Fake version of IssueService that just works in-RAM."""
1535 # pylint: disable=unused-argument
1536
1537 def __init__(self, user_id=None):
1538 self.user_id = user_id
1539 # Dictionary {project_id: issue_pb_dict}
1540 # where issue_pb_dict is a dictionary of the form
1541 # {local_id: issue_pb}
1542 self.issues_by_project = {}
1543 self.issues_by_iid = {}
1544 # Dictionary {project_id: comment_pb_dict}
1545 # where comment_pb_dict is a dictionary of the form
1546 # {local_id: comment_pb_list}
1547 self.comments_by_project = {}
1548 self.comments_by_iid = {}
1549 self.comments_by_cid = {}
1550 self.attachments_by_id = {}
1551
1552 # Set of issue IDs for issues that have been indexed by calling
1553 # IndexIssues().
1554 self.indexed_issue_iids = set()
1555
1556 # Set of issue IDs for issues that have been moved by calling MoveIssue().
1557 self.moved_back_iids = set()
1558
1559 # Dict of issue IDs mapped to other issue IDs to represent moved issues.
1560 self.moved_issues = {}
1561
1562 # Test-only indication that the indexer would have been called
1563 # by the real DITPersist.
1564 self.indexer_called = False
1565
1566 # Test-only sequence of updated and enqueued.
1567 self.updated_issues = []
1568 self.enqueued_issues = [] # issue_ids
1569
1570 # Test-only sequence of expunged issues and projects.
1571 self.expunged_issues = []
1572 self.expunged_former_locations = []
1573 self.expunged_local_ids = []
1574 self.expunged_users_in_issues = []
1575
1576 # Test-only indicators that methods were called.
1577 self.get_all_issues_in_project_called = False
1578 self.update_issues_called = False
1579 self.enqueue_issues_called = False
1580 self.get_issue_acitivity_called = False
1581
1582 # The next id to return if it is > 0.
1583 self.next_id = -1
1584
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01001585 def UpdateIssue(
1586 self,
1587 cnxn,
1588 issue,
1589 update_cols=None,
1590 just_derived=False,
1591 commit=True,
1592 invalidate=True):
1593 self.UpdateIssues(
1594 cnxn, [issue], update_cols, just_derived, commit, invalidate)
1595
Copybara854996b2021-09-07 19:36:02 +00001596 def UpdateIssues(
1597 self, cnxn, issues, update_cols=None, just_derived=False,
1598 commit=True, invalidate=True):
1599 self.update_issues_called = True
1600 assert all(issue.assume_stale == False for issue in issues)
1601 self.updated_issues.extend(issues)
1602
1603 def GetIssueActivity(
1604 self, cnxn, num=50, before=None, after=None,
1605 project_ids=None, user_ids=None, ascending=False):
1606 self.get_issue_acitivity_called = True
1607 comments_dict = self.comments_by_cid
1608 comments = []
1609 for value in comments_dict.values():
1610 if project_ids is not None:
1611 if value.issue_id > 0 and value.issue_id in self.issues_by_iid:
1612 issue = self.issues_by_iid[value.issue_id]
1613 if issue.project_id in project_ids:
1614 comments.append(value)
1615 elif user_ids is not None:
1616 if value.user_id in user_ids:
1617 comments.append(value)
1618 else:
1619 comments.append(value)
1620 return comments
1621
1622 def EnqueueIssuesForIndexing(self, _cnxn, issue_ids, commit=True):
1623 self.enqueue_issues_called = True
1624 for i in issue_ids:
1625 if i not in self.enqueued_issues:
1626 self.enqueued_issues.extend(issues)
1627
1628 def ExpungeIssues(self, _cnxn, issue_ids):
1629 self.expunged_issues.extend(issue_ids)
1630
1631 def ExpungeFormerLocations(self, _cnxn, project_id):
1632 self.expunged_former_locations.append(project_id)
1633
1634 def ExpungeLocalIDCounters(self, _cnxn, project_id):
1635 self.expunged_local_ids.append(project_id)
1636
1637 def TestAddIssue(self, issue, importer_id=None):
1638 project_id = issue.project_id
1639 self.issues_by_project.setdefault(project_id, {})
1640 self.issues_by_project[project_id][issue.local_id] = issue
1641 self.issues_by_iid[issue.issue_id] = issue
1642 if issue.issue_id not in self.enqueued_issues:
1643 self.enqueued_issues.append(issue.issue_id)
1644 self.enqueue_issues_called = True
1645
1646 # Adding a new issue should add the first comment to the issue
1647 comment = tracker_pb2.IssueComment()
1648 comment.project_id = issue.project_id
1649 comment.issue_id = issue.issue_id
1650 comment.content = issue.summary
1651 comment.timestamp = issue.opened_timestamp
1652 comment.is_description = True
1653 if issue.reporter_id:
1654 comment.user_id = issue.reporter_id
1655 if importer_id:
1656 comment.importer_id = importer_id
1657 comment.sequence = 0
1658 self.TestAddComment(comment, issue.local_id)
1659
1660 def TestAddMovedIssueRef(self, source_project_id, source_local_id,
1661 target_project_id, target_local_id):
1662 self.moved_issues[(source_project_id, source_local_id)] = (
1663 target_project_id, target_local_id)
1664
1665 def TestAddComment(self, comment, local_id):
1666 pid = comment.project_id
1667 if not comment.id:
1668 comment.id = len(self.comments_by_cid)
1669
1670 self.comments_by_project.setdefault(pid, {})
1671 self.comments_by_project[pid].setdefault(local_id, []).append(comment)
1672 self.comments_by_iid.setdefault(comment.issue_id, []).append(comment)
1673 self.comments_by_cid[comment.id] = comment
1674
1675 def TestAddAttachment(self, attachment, comment_id, issue_id):
1676 if not attachment.attachment_id:
1677 attachment.attachment_id = len(self.attachments_by_id)
1678
1679 aid = attachment.attachment_id
1680 self.attachments_by_id[aid] = attachment, comment_id, issue_id
1681 comment = self.comments_by_cid[comment_id]
1682 if attachment not in comment.attachments:
1683 comment.attachments.extend([attachment])
1684
1685 def SoftDeleteAttachment(
1686 self, _cnxn, _issue, comment, attach_id, _user_service, delete=True,
1687 index_now=False):
1688 attachment = None
1689 for attach in comment.attachments:
1690 if attach.attachment_id == attach_id:
1691 attachment = attach
1692 if not attachment:
1693 return
1694 attachment.deleted = delete
1695
1696 def GetAttachmentAndContext(self, _cnxn, attachment_id):
1697 if attachment_id in self.attachments_by_id:
1698 attach, comment_id, issue_id = self.attachments_by_id[attachment_id]
1699 if not attach.deleted:
1700 return attach, comment_id, issue_id
1701
1702 raise exceptions.NoSuchAttachmentException()
1703
1704 def GetComments(
1705 self, _cnxn, where=None, order_by=None, content_only=False, **kwargs):
1706 # This is a very limited subset of what the real GetComments() can do.
1707 cid = kwargs.get('id')
1708
1709 comment = self.comments_by_cid.get(cid)
1710 if comment:
1711 return [comment]
1712 else:
1713 return []
1714
1715 def GetComment(self, cnxn, comment_id):
1716 """Get the requested comment, or raise an exception."""
1717 comments = self.GetComments(cnxn, id=comment_id)
1718 if len(comments) == 1:
1719 return comments[0]
1720
1721 raise exceptions.NoSuchCommentException()
1722
1723 def ResolveIssueRefs(self, cnxn, ref_projects, default_project_name, refs):
1724 result = []
1725 misses = []
1726 for project_name, local_id in refs:
1727 project = ref_projects.get(project_name or default_project_name)
1728 if not project or project.state == project_pb2.ProjectState.DELETABLE:
1729 continue # ignore any refs to issues in deleted projects
1730 try:
1731 issue = self.GetIssueByLocalID(cnxn, project.project_id, local_id)
1732 result.append(issue.issue_id)
1733 except exceptions.NoSuchIssueException:
1734 misses.append((project.project_id, local_id))
1735
1736 return result, misses
1737
1738 def LookupIssueRefs(self, cnxn, issue_ids):
1739 issue_dict, _misses = self.GetIssuesDict(cnxn, issue_ids)
1740 return {
1741 issue_id: (issue.project_name, issue.local_id)
1742 for issue_id, issue in issue_dict.items()}
1743
1744 def GetAllIssuesInProject(
1745 self, _cnxn, project_id, min_local_id=None, use_cache=True):
1746 self.get_all_issues_in_project_called = True
1747 if project_id in self.issues_by_project:
1748 return list(self.issues_by_project[project_id].values())
1749 else:
1750 return []
1751
1752 def GetIssuesByLocalIDs(
1753 self, _cnxn, project_id, local_id_list, use_cache=True, shard_id=None):
1754 results = []
1755 for local_id in local_id_list:
1756 if (project_id in self.issues_by_project
1757 and local_id in self.issues_by_project[project_id]):
1758 results.append(self.issues_by_project[project_id][local_id])
1759
1760 return results
1761
1762 def GetIssueByLocalID(self, _cnxn, project_id, local_id, use_cache=True):
1763 try:
1764 return self.issues_by_project[project_id][local_id]
1765 except KeyError:
1766 raise exceptions.NoSuchIssueException()
1767
1768 def GetAnyOnHandIssue(self, issue_ids, start=None, end=None):
1769 return None # Treat them all like misses.
1770
1771 def GetIssue(self, cnxn, issue_id, use_cache=True):
1772 issues = self.GetIssues(cnxn, [issue_id], use_cache=use_cache)
1773 try:
1774 return issues[0]
1775 except IndexError:
1776 raise exceptions.NoSuchIssueException()
1777
1778 def GetCurrentLocationOfMovedIssue(self, cnxn, project_id, local_id):
1779 key = (project_id, local_id)
1780 if key in self.moved_issues:
1781 ref = self.moved_issues[key]
1782 return ref[0], ref[1]
1783 return None, None
1784
1785 def GetPreviousLocations(self, cnxn, issue):
1786 return []
1787
1788 def GetCommentsByUser(self, cnxn, user_id):
1789 """Get all comments created by a user"""
1790 comments = []
1791 for cid in self.comments_by_cid:
1792 comment = self.comments_by_cid[cid]
1793 if comment.user_id == user_id and not comment.is_description:
1794 comments.append(comment)
1795 return comments
1796
1797 def GetCommentsByID(self, cnxn, comment_ids, _sequences, use_cache=True,
1798 shard_id=None):
1799 """Return all IssueComment PBs by comment ids."""
1800 comments = [self.comments_by_cid[cid] for cid in comment_ids]
1801 return comments
1802
1803 def GetIssueIDsReportedByUser(self, cnxn, user_id):
1804 """Get all issues created by a user"""
1805 ids = []
1806 for iid in self.issues_by_iid:
1807 issue = self.issues_by_iid[iid]
1808 if issue.reporter_id == user_id:
1809 ids.append(iid)
1810 return ids
1811
1812 def LookupIssueIDs(self, _cnxn, project_local_id_pairs):
1813 hits = []
1814 misses = []
1815 for (project_id, local_id) in project_local_id_pairs:
1816 try:
1817 issue = self.issues_by_project[project_id][local_id]
1818 hits.append(issue.issue_id)
1819 except KeyError:
1820 misses.append((project_id, local_id))
1821
1822 return hits, misses
1823
1824 def LookupIssueIDsFollowMoves(self, _cnxn, project_local_id_pairs):
1825 hits = []
1826 misses = []
1827 for pair in project_local_id_pairs:
1828 project_id, local_id = self.moved_issues.get(pair, pair)
1829 try:
1830 issue = self.issues_by_project[project_id][local_id]
1831 hits.append(issue.issue_id)
1832 except KeyError:
1833 misses.append((project_id, local_id))
1834
1835 return hits, misses
1836
1837 def LookupIssueID(self, _cnxn, project_id, local_id):
1838 try:
1839 issue = self.issues_by_project[project_id][local_id]
1840 except KeyError:
1841 raise exceptions.NoSuchIssueException()
1842 return issue.issue_id
1843
1844 def GetCommentsForIssue(self, _cnxn, issue_id):
1845 comments = self.comments_by_iid.get(issue_id, [])
1846 for idx, c in enumerate(comments):
1847 c.sequence = idx
1848
1849 return comments
1850
1851 def InsertIssue(self, cnxn, issue):
1852 issue.issue_id = issue.project_id * 1000000 + issue.local_id
1853 self.issues_by_project.setdefault(issue.project_id, {})
1854 self.issues_by_project[issue.project_id][issue.local_id] = issue
1855 self.issues_by_iid[issue.issue_id] = issue
1856 return issue.issue_id
1857
1858 def CreateIssue(
1859 self,
1860 cnxn,
1861 services,
1862 issue,
1863 marked_description,
1864 attachments=None,
1865 index_now=False,
1866 importer_id=None):
1867 project_id = issue.project_id
1868
1869 issue.local_id = self.AllocateNextLocalID(cnxn, project_id)
1870 issue.issue_id = project_id * 1000000 + issue.local_id
1871
1872 self.TestAddIssue(issue, importer_id=importer_id)
1873 comment = self.comments_by_iid[issue.issue_id][0]
1874 comment.content = marked_description
1875 return issue, comment
1876
1877 def GetIssueApproval(self, cnxn, issue_id, approval_id, use_cache=True):
1878 issue = self.GetIssue(cnxn, issue_id, use_cache=use_cache)
1879 approval = tracker_bizobj.FindApprovalValueByID(
1880 approval_id, issue.approval_values)
1881 if approval:
1882 return issue, approval
1883 raise exceptions.NoSuchIssueApprovalException()
1884
1885 def UpdateIssueApprovalStatus(
1886 self, cnxn, issue_id, approval_id, status, setter_id, set_on,
1887 commit=True):
1888 issue = self.GetIssue(cnxn, issue_id)
1889 for av in issue.approval_values:
1890 if av.approval_id == approval_id:
1891 av.status = status
1892 av.setter_id = setter_id
1893 av.set_on = set_on
1894 return
1895 return
1896
1897 def UpdateIssueApprovalApprovers(
1898 self, cnxn, issue_id, approval_id, approver_ids, commit=True):
1899 issue = self.GetIssue(cnxn, issue_id)
1900 for av in issue.approval_values:
1901 if av.approval_id == approval_id:
1902 av.approver_ids = approver_ids
1903 return
1904 return
1905
1906 def UpdateIssueStructure(
1907 self, cnxn, config, issue, template, reporter_id, comment_content,
1908 commit=True, invalidate=True):
1909 approval_defs_by_id = {ad.approval_id: ad for ad in config.approval_defs}
1910 issue_avs_by_id = {av.approval_id: av for av in issue.approval_values}
1911
1912 new_issue_approvals = []
1913
1914 for template_av in template.approval_values:
1915 existing_issue_av = issue_avs_by_id.get(template_av.approval_id)
1916 # Keep approval values as-if fi it exists in issue and template
1917 if existing_issue_av:
1918 existing_issue_av.phase_id = template_av.phase_id
1919 new_issue_approvals.append(existing_issue_av)
1920 else:
1921 new_issue_approvals.append(template_av)
1922
1923 # Update all approval surveys so latest ApprovalDef survey changes
1924 # appear in the converted issue's approval values.
1925 ad = approval_defs_by_id.get(template_av.approval_id)
1926 if ad:
1927 self.CreateIssueComment(
1928 cnxn, issue, reporter_id, ad.survey,
1929 is_description=True, approval_id=ad.approval_id, commit=False)
1930 else:
1931 logging.info('ApprovalDef not found for approval %r', template_av)
1932
1933 template_phase_by_name = {
1934 phase.name.lower(): phase for phase in template.phases}
1935 issue_phase_by_id = {phase.phase_id: phase for phase in issue.phases}
1936 updated_fvs = []
1937 # Trim issue FieldValues or update FieldValue phase_ids
1938 for fv in issue.field_values:
1939 # If a fv's phase has the same name as a template's phase, update
1940 # the fv's phase_id to that of the template phase's. Otherwise,
1941 # remove the fv.
1942 if fv.phase_id:
1943 issue_phase = issue_phase_by_id.get(fv.phase_id)
1944 if issue_phase and issue_phase.name:
1945 template_phase = template_phase_by_name.get(issue_phase.name.lower())
1946 if template_phase:
1947 fv.phase_id = template_phase.phase_id
1948 updated_fvs.append(fv)
1949 # keep all fvs that do not belong to phases.
1950 else:
1951 updated_fvs.append(fv)
1952
1953 fd_names_by_id = {fd.field_id: fd.field_name for fd in config.field_defs}
1954 amendment = tracker_bizobj.MakeApprovalStructureAmendment(
1955 [fd_names_by_id.get(av.approval_id) for av in new_issue_approvals],
1956 [fd_names_by_id.get(av.approval_id) for av in issue.approval_values])
1957
1958 issue.approval_values = new_issue_approvals
1959 issue.phases = template.phases
1960 issue.field_values = updated_fvs
1961
1962 return self.CreateIssueComment(
1963 cnxn, issue, reporter_id, comment_content,
1964 amendments=[amendment], commit=False)
1965
1966 def SetUsedLocalID(self, cnxn, project_id):
1967 self.next_id = self.GetHighestLocalID(cnxn, project_id) + 1
1968
1969 def AllocateNextLocalID(self, cnxn, project_id):
1970 return self.GetHighestLocalID(cnxn, project_id) + 1
1971
1972 def GetHighestLocalID(self, _cnxn, project_id):
1973 if self.next_id > 0:
1974 return self.next_id - 1
1975 else:
1976 issue_dict = self.issues_by_project.get(project_id, {})
1977 highest = max([0] + [issue.local_id for issue in issue_dict.values()])
1978 return highest
1979
1980 def _MakeIssueComment(
1981 self, project_id, user_id, content, inbound_message=None,
1982 amendments=None, attachments=None, kept_attachments=None, timestamp=None,
1983 is_spam=False, is_description=False, approval_id=None, importer_id=None):
1984 comment = tracker_pb2.IssueComment()
1985 comment.project_id = project_id
1986 comment.user_id = user_id
1987 comment.content = content or ''
1988 comment.is_spam = is_spam
1989 comment.is_description = is_description
1990 if not timestamp:
1991 timestamp = int(time.time())
1992 comment.timestamp = int(timestamp)
1993 if inbound_message:
1994 comment.inbound_message = inbound_message
1995 if amendments:
1996 comment.amendments.extend(amendments)
1997 if approval_id:
1998 comment.approval_id = approval_id
1999 if importer_id:
2000 comment.importer_id = importer_id
2001 return comment
2002
2003 def CopyIssues(self, cnxn, dest_project, issues, user_service, copier_id):
2004 created_issues = []
2005 for target_issue in issues:
2006 new_issue = tracker_pb2.Issue()
2007 new_issue.project_id = dest_project.project_id
2008 new_issue.project_name = dest_project.project_name
2009 new_issue.summary = target_issue.summary
2010 new_issue.labels.extend(target_issue.labels)
2011 new_issue.field_values.extend(target_issue.field_values)
2012 new_issue.reporter_id = copier_id
2013
2014 timestamp = int(time.time())
2015 new_issue.opened_timestamp = timestamp
2016 new_issue.modified_timestamp = timestamp
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01002017 new_issue.migration_modified_timestamp = timestamp
Copybara854996b2021-09-07 19:36:02 +00002018
2019 target_comments = self.GetCommentsForIssue(cnxn, target_issue.issue_id)
2020 initial_summary_comment = target_comments[0]
2021
2022 # Note that blocking and merge_into are not copied.
2023 new_issue.blocked_on_iids = target_issue.blocked_on_iids
2024 new_issue.blocked_on_ranks = target_issue.blocked_on_ranks
2025
2026 # Create the same summary comment as the target issue.
2027 comment = self._MakeIssueComment(
2028 dest_project.project_id, copier_id, initial_summary_comment.content,
2029 is_description=True)
2030
2031 new_issue.local_id = self.AllocateNextLocalID(
2032 cnxn, dest_project.project_id)
2033 issue_id = self.InsertIssue(cnxn, new_issue)
2034 comment.issue_id = issue_id
2035 self.InsertComment(cnxn, comment)
2036 created_issues.append(new_issue)
2037
2038 return created_issues
2039
2040 def MoveIssues(self, cnxn, dest_project, issues, user_service):
2041 move_to = dest_project.project_id
2042 self.issues_by_project.setdefault(move_to, {})
2043 moved_back_iids = set()
2044 for issue in issues:
2045 if issue.issue_id in self.moved_back_iids:
2046 moved_back_iids.add(issue.issue_id)
2047 self.moved_back_iids.add(issue.issue_id)
2048 project_id = issue.project_id
2049 self.issues_by_project[project_id].pop(issue.local_id)
2050 issue.local_id = self.AllocateNextLocalID(cnxn, move_to)
2051 self.issues_by_project[move_to][issue.local_id] = issue
2052 issue.project_id = move_to
2053 issue.project_name = dest_project.project_name
2054 return moved_back_iids
2055
2056 def GetCommentsForIssues(self, _cnxn, issue_ids, content_only=False):
2057 comments_dict = {}
2058 for issue_id in issue_ids:
2059 comments_dict[issue_id] = self.comments_by_iid[issue_id]
2060
2061 return comments_dict
2062
2063 def InsertComment(self, cnxn, comment, commit=True):
2064 issue = self.GetIssue(cnxn, comment.issue_id)
2065 self.TestAddComment(comment, issue.local_id)
2066
2067 # pylint: disable=unused-argument
2068 def DeltaUpdateIssue(
2069 self, cnxn, services, reporter_id, project_id,
2070 config, issue, delta, index_now=False, comment=None, attachments=None,
2071 iids_to_invalidate=None, rules=None, predicate_asts=None,
2072 is_description=False, timestamp=None, kept_attachments=None,
2073 importer_id=None, inbound_message=None):
2074 # Return a bogus amendments list if any of the fields changed
2075 amendments, _ = tracker_bizobj.ApplyIssueDelta(
2076 cnxn, self, issue, delta, config)
2077
2078 if not amendments and (not comment or not comment.strip()):
2079 return [], None
2080
2081 comment_pb = self.CreateIssueComment(
2082 cnxn, issue, reporter_id, comment, attachments=attachments,
2083 amendments=amendments, is_description=is_description,
2084 kept_attachments=kept_attachments, importer_id=importer_id,
2085 inbound_message=inbound_message)
2086
2087 self.indexer_called = index_now
2088 return amendments, comment_pb
2089
2090 def InvalidateIIDs(self, cnxn, iids_to_invalidate):
2091 pass
2092
2093 # pylint: disable=unused-argument
2094 def CreateIssueComment(
2095 self, _cnxn, issue, user_id, content,
2096 inbound_message=None, amendments=None, attachments=None,
2097 kept_attachments=None, timestamp=None, is_spam=False,
2098 is_description=False, approval_id=None, commit=True,
2099 importer_id=None):
2100 # Add a comment to an issue
2101 comment = tracker_pb2.IssueComment()
2102 comment.id = len(self.comments_by_cid)
2103 comment.project_id = issue.project_id
2104 comment.issue_id = issue.issue_id
2105 comment.content = content
2106 comment.user_id = user_id
2107 if timestamp is not None:
2108 comment.timestamp = timestamp
2109 else:
2110 comment.timestamp = 1234567890
2111 if amendments:
2112 comment.amendments.extend(amendments)
2113 if inbound_message:
2114 comment.inbound_message = inbound_message
2115 comment.is_spam = is_spam
2116 comment.is_description = is_description
2117 if approval_id:
2118 comment.approval_id = approval_id
2119
2120 pid = issue.project_id
2121 self.comments_by_project.setdefault(pid, {})
2122 self.comments_by_project[pid].setdefault(issue.local_id, []).append(comment)
2123 self.comments_by_iid.setdefault(issue.issue_id, []).append(comment)
2124 self.comments_by_cid[comment.id] = comment
2125
2126 if attachments:
2127 for filename, filecontent, mimetype in attachments:
2128 aid = len(self.attachments_by_id)
2129 attach = tracker_pb2.Attachment(
2130 attachment_id=aid,
2131 filename=filename,
2132 filesize=len(filecontent),
2133 mimetype=mimetype,
2134 gcs_object_id='gcs_object_id(%s)' % filename)
2135 comment.attachments.append(attach)
2136 self.attachments_by_id[aid] = attach, pid, comment.id
2137
2138 if kept_attachments:
2139 comment.attachments.extend([
2140 self.attachments_by_id[aid][0]
2141 for aid in kept_attachments])
2142
2143 return comment
2144
2145 def GetOpenAndClosedIssues(self, _cnxn, issue_ids):
2146 open_issues = []
2147 closed_issues = []
2148 for issue_id in issue_ids:
2149 try:
2150 issue = self.issues_by_iid[issue_id]
2151 if issue.status == 'Fixed':
2152 closed_issues.append(issue)
2153 else:
2154 open_issues.append(issue)
2155 except KeyError:
2156 continue
2157
2158 return open_issues, closed_issues
2159
2160 def GetIssuesDict(
2161 self, _cnxn, issue_ids, use_cache=True, shard_id=None):
2162 missing_ids = [iid for iid in issue_ids if iid not in self.issues_by_iid]
2163 issues_by_id = {}
2164 for iid in issue_ids:
2165 if iid in self.issues_by_iid:
2166 issue = self.issues_by_iid[iid]
2167 if not use_cache:
2168 issue.assume_stale = False
2169 issues_by_id[iid] = issue
2170
2171 return issues_by_id, missing_ids
2172
2173 def GetIssues(self, cnxn, issue_ids, use_cache=True, shard_id=None):
2174 issues_by_iid, _misses = self.GetIssuesDict(
2175 cnxn, issue_ids, use_cache=use_cache, shard_id=shard_id)
2176 results = [
2177 issues_by_iid[issue_id]
2178 for issue_id in issue_ids
2179 if issue_id in issues_by_iid
2180 ]
2181
2182 return results
2183
2184 def SoftDeleteIssue(
2185 self, _cnxn, project_id, local_id, deleted, user_service):
2186 issue = self.issues_by_project[project_id][local_id]
2187 issue.deleted = deleted
2188
2189 def SoftDeleteComment(
2190 self, cnxn, issue, comment, deleted_by_user_id, user_service,
2191 delete=True, reindex=False, is_spam=False):
2192 pid = comment.project_id
2193 # Find the original comment by the sequence number.
2194 c = None
2195 by_iid_idx = -1
2196 for by_iid_idx, c in enumerate(self.comments_by_iid[issue.issue_id]):
2197 if c.sequence == comment.sequence:
2198 break
2199 comment = c
2200 by_project_idx = (
2201 self.comments_by_project[pid][issue.local_id].index(comment))
2202 comment.is_spam = is_spam
2203 if delete:
2204 comment.deleted_by = deleted_by_user_id
2205 else:
2206 comment.reset('deleted_by')
2207 self.comments_by_project[pid][issue.local_id][by_project_idx] = comment
2208 self.comments_by_iid[issue.issue_id][by_iid_idx] = comment
2209 self.comments_by_cid[comment.id] = comment
2210
2211 def DeleteComponentReferences(self, _cnxn, component_id):
2212 for _, issue in self.issues_by_iid.items():
2213 issue.component_ids = [
2214 cid for cid in issue.component_ids if cid != component_id]
2215
2216 def RunIssueQuery(
2217 self, cnxn, left_joins, where, order_by, shard_id=None, limit=None):
2218 """This always returns empty results. Mock it to test other cases."""
2219 return [], False
2220
2221 def GetIIDsByLabelIDs(self, cnxn, label_ids, project_id, shard_id):
2222 """This always returns empty results. Mock it to test other cases."""
2223 return []
2224
2225 def GetIIDsByParticipant(self, cnxn, user_ids, project_ids, shard_id):
2226 """This always returns empty results. Mock it to test other cases."""
2227 return []
2228
2229 def SortBlockedOn(self, cnxn, issue, blocked_on_iids):
2230 return blocked_on_iids, [0] * len(blocked_on_iids)
2231
2232 def ApplyIssueRerank(
2233 self, cnxn, parent_id, relations_to_change, commit=True, invalidate=True):
2234 issue = self.GetIssue(cnxn, parent_id)
2235 relations_dict = dict(
2236 list(zip(issue.blocked_on_iids, issue.blocked_on_ranks)))
2237 relations_dict.update(relations_to_change)
2238 issue.blocked_on_ranks = sorted(issue.blocked_on_ranks, reverse=True)
2239 issue.blocked_on_iids = sorted(
2240 issue.blocked_on_iids, key=relations_dict.get, reverse=True)
2241
2242 def SplitRanks(self, cnxn, parent_id, target_id, open_ids, split_above=False):
2243 pass
2244
2245 def ExpungeUsersInIssues(self, cnxn, user_ids_by_email, limit=None):
2246 user_ids = list(user_ids_by_email.values())
2247 self.expunged_users_in_issues.extend(user_ids)
2248 return []
2249
2250
2251class TemplateService(object):
2252 """Fake version of TemplateService that just works in-RAM."""
2253
2254 def __init__(self):
2255 self.templates_by_id = {} # template_id: template_pb
2256 self.templates_by_project_id = {} # project_id: [template_id]
2257
2258 def TestAddIssueTemplateDef(
2259 self, template_id, project_id, name, content="", summary="",
2260 summary_must_be_edited=False, status='New', members_only=False,
2261 owner_defaults_to_member=False, component_required=False, owner_id=None,
2262 labels=None, component_ids=None, admin_ids=None, field_values=None,
2263 phases=None, approval_values=None):
2264 template = tracker_bizobj.MakeIssueTemplate(
2265 name,
2266 summary,
2267 status,
2268 owner_id,
2269 content,
2270 labels,
2271 field_values or [],
2272 admin_ids or [],
2273 component_ids,
2274 summary_must_be_edited=summary_must_be_edited,
2275 owner_defaults_to_member=owner_defaults_to_member,
2276 component_required=component_required,
2277 members_only=members_only,
2278 phases=phases,
2279 approval_values=approval_values)
2280 template.template_id = template_id
2281 self.templates_by_id[template_id] = template
2282 if project_id not in self.templates_by_project_id:
2283 self.templates_by_project_id[project_id] = []
2284 self.templates_by_project_id[project_id].append(template_id)
2285 return template
2286
2287 def GetTemplateByName(self, cnxn, template_name, project_id):
2288 if project_id not in self.templates_by_project_id:
2289 return None
2290 else:
2291 project_templates = self.templates_by_project_id[project_id]
2292 for template_id in project_templates:
2293 template = self.GetTemplateById(cnxn, template_id)
2294 if template.name == template_name:
2295 return template
2296 return None
2297
2298 def GetTemplateById(self, cnxn, template_id):
2299 return self.templates_by_id.get(template_id)
2300
2301 def GetTemplatesById(self, cnxn, template_ids):
2302 return filter(
2303 lambda template: template.template_id in template_ids,
2304 self.templates_by_id.values())
2305
2306 def GetProjectTemplates(self, cnxn, project_id):
2307 template_ids = self.templates_by_project_id[project_id]
2308 return self.GetTemplatesById(cnxn, template_ids)
2309
2310 def ExpungeUsersInTemplates(self, cnxn, user_ids, limit=None):
2311 for _, template in self.templates_by_id.items():
2312 template.admin_ids = [user_id for user_id in template.admin_ids
2313 if user_id not in user_ids]
2314 if template.owner_id in user_ids:
2315 template.owner_id = None
2316 template.field_values = [fv for fv in template.field_values
2317 if fv.user_id in user_ids]
2318
2319class SpamService(object):
2320 """Fake version of SpamService that just works in-RAM."""
2321
2322 def __init__(self, user_id=None):
2323 self.user_id = user_id
2324 self.reports_by_issue_id = collections.defaultdict(list)
2325 self.comment_reports_by_issue_id = collections.defaultdict(dict)
2326 self.manual_verdicts_by_issue_id = collections.defaultdict(dict)
2327 self.manual_verdicts_by_comment_id = collections.defaultdict(dict)
2328 self.expunged_users_in_spam = []
2329
2330 def LookupIssuesFlaggers(self, cnxn, issue_ids):
2331 return {
2332 issue_id: (self.reports_by_issue_id.get(issue_id, []),
2333 self.comment_reports_by_issue_id.get(issue_id, {}))
2334 for issue_id in issue_ids}
2335
2336 def LookupIssueFlaggers(self, cnxn, issue_id):
2337 return self.LookupIssuesFlaggers(cnxn, [issue_id])[issue_id]
2338
2339 def FlagIssues(self, cnxn, issue_service, issues, user_id, flagged_spam):
2340 for issue in issues:
2341 if flagged_spam:
2342 self.reports_by_issue_id[issue.issue_id].append(user_id)
2343 else:
2344 self.reports_by_issue_id[issue.issue_id].remove(user_id)
2345
2346 def FlagComment(
2347 self, cnxn, issue, comment_id, reported_user_id, user_id, flagged_spam):
2348 if not comment_id in self.comment_reports_by_issue_id[issue.issue_id]:
2349 self.comment_reports_by_issue_id[issue.issue_id][comment_id] = []
2350 if flagged_spam:
2351 self.comment_reports_by_issue_id[issue.issue_id][comment_id].append(
2352 user_id)
2353 else:
2354 self.comment_reports_by_issue_id[issue.issue_id][comment_id].remove(
2355 user_id)
2356
2357 def RecordManualIssueVerdicts(
2358 self, cnxn, issue_service, issues, user_id, is_spam):
2359 for issue in issues:
2360 self.manual_verdicts_by_issue_id[issue.issue_id][user_id] = is_spam
2361
2362 def RecordManualCommentVerdict(
2363 self, cnxn, issue_service, user_service, comment_id,
2364 user_id, is_spam):
2365 self.manual_verdicts_by_comment_id[comment_id][user_id] = is_spam
2366 comment = issue_service.GetComment(cnxn, comment_id)
2367 comment.is_spam = is_spam
2368 issue = issue_service.GetIssue(cnxn, comment.issue_id, use_cache=False)
2369 issue_service.SoftDeleteComment(
2370 cnxn, issue, comment, user_id, user_service, is_spam, True, is_spam)
2371
2372 def RecordClassifierIssueVerdict(self, cnxn, issue, is_spam, confidence,
2373 failed_open):
2374 return
2375
2376 def RecordClassifierCommentVerdict(self, cnxn, issue, is_spam, confidence,
2377 failed_open):
2378 return
2379
2380 def ClassifyComment(self, comment, commenter):
2381 return {'outputLabel': 'ham',
2382 'outputMulti': [{'label': 'ham', 'score': '1.0'}],
2383 'failed_open': False}
2384
2385 def ClassifyIssue(self, issue, firstComment, reporter):
2386 return {'outputLabel': 'ham',
2387 'outputMulti': [{'label': 'ham', 'score': '1.0'}],
2388 'failed_open': False}
2389
2390 def ExpungeUsersInSpam(self, cnxn, user_ids):
2391 self.expunged_users_in_spam.extend(user_ids)
2392
2393
2394class FeaturesService(object):
2395 """A fake implementation of FeaturesService."""
2396 def __init__(self):
2397 # Test-only sequence of expunged projects and users.
2398 self.expunged_saved_queries = []
2399 self.expunged_users_in_saved_queries = []
2400 self.expunged_filter_rules = []
2401 self.expunged_users_in_filter_rules = []
2402 self.expunged_quick_edit = []
2403 self.expunged_users_in_quick_edits = []
2404 self.expunged_hotlist_ids = []
2405 self.expunged_users_in_hotlists = []
2406
2407 # filter rules, project_id => filterrule_pb
2408 self.test_rules = collections.defaultdict(list)
2409
2410 # TODO(crbug/monorail/7104): Confirm that these are never reassigned
2411 # to empty {} and then change these to collections.defaultdicts instead.
2412 # hotlists
2413 self.test_hotlists = {} # (hotlist_name, owner_id) => hotlist_pb
2414 self.hotlists_by_id = {}
2415 self.hotlists_id_by_user = {} # user_id => [hotlist_id, hotlist_id, ...]
2416 self.hotlists_id_by_issue = {} # issue_id => [hotlist_id, hotlist_id, ...]
2417
2418 # saved queries
2419 self.saved_queries = [] # [(pid, uid, sq), ...]
2420
2421 def TestAddFilterRule(
2422 self, project_id, predicate, default_status=None, default_owner_id=None,
2423 add_cc_ids=None, add_labels=None, add_notify=None, warning=None,
2424 error=None):
2425 rule = filterrules_helpers.MakeRule(
2426 predicate, default_status=default_status,
2427 default_owner_id=default_owner_id, add_cc_ids=add_cc_ids,
2428 add_labels=add_labels, add_notify=add_notify, warning=warning,
2429 error=error)
2430 self.test_rules[project_id].append(rule)
2431 return rule
2432
2433 def TestAddHotlist(self, name, summary='', owner_ids=None, editor_ids=None,
2434 follower_ids=None, description=None, hotlist_id=None,
2435 is_private=False, hotlist_item_fields=None,
2436 default_col_spec=None):
2437 """Add a hotlist to the fake FeaturesService object.
2438
2439 Args:
2440 name: the name of the hotlist. Will replace any existing hotlist under
2441 the same name.
2442 summary: the summary string of the hotlist
2443 owner_ids: List of user ids for the hotlist owners
2444 editor_ids: List of user ids for the hotlist editors
2445 follower_ids: List of user ids for the hotlist followers
2446 description: The description string for this hotlist
2447 hotlist_id: A unique integer identifier for the created hotlist
2448 is_private: A boolean indicating whether the hotlist is private/public
2449 hotlist_item_fields: a list of tuples ->
2450 [(issue_id, rank, adder_id, date_added, note),...]
2451 default_col_spec: string of default columns for the hotlist.
2452
2453 Returns:
2454 A populated hotlist PB.
2455 """
2456 hotlist_pb = features_pb2.Hotlist()
2457 hotlist_pb.hotlist_id = hotlist_id or hash(name) % 100000
2458 hotlist_pb.name = name
2459 hotlist_pb.summary = summary
2460 hotlist_pb.is_private = is_private
2461 hotlist_pb.default_col_spec = default_col_spec
2462 if description is not None:
2463 hotlist_pb.description = description
2464
2465 self.TestAddHotlistMembers(owner_ids, hotlist_pb, OWNER_ROLE)
2466 self.TestAddHotlistMembers(follower_ids, hotlist_pb, FOLLOWER_ROLE)
2467 self.TestAddHotlistMembers(editor_ids, hotlist_pb, EDITOR_ROLE)
2468
2469 if hotlist_item_fields is not None:
2470 for(issue_id, rank, adder_id, date, note) in hotlist_item_fields:
2471 hotlist_pb.items.append(
2472 features_pb2.Hotlist.HotlistItem(
2473 issue_id=issue_id, rank=rank, adder_id=adder_id,
2474 date_added=date, note=note))
2475 try:
2476 self.hotlists_id_by_issue[issue_id].append(hotlist_pb.hotlist_id)
2477 except KeyError:
2478 self.hotlists_id_by_issue[issue_id] = [hotlist_pb.hotlist_id]
2479
2480 owner_id = None
2481 if hotlist_pb.owner_ids:
2482 owner_id = hotlist_pb.owner_ids[0]
2483 self.test_hotlists[(name, owner_id)] = hotlist_pb
2484 self.hotlists_by_id[hotlist_pb.hotlist_id] = hotlist_pb
2485 return hotlist_pb
2486
2487 def TestAddHotlistMembers(self, user_id_list, hotlist_pb, role):
2488 if user_id_list is not None:
2489 for user_id in user_id_list:
2490 if role == OWNER_ROLE:
2491 hotlist_pb.owner_ids.append(user_id)
2492 elif role == EDITOR_ROLE:
2493 hotlist_pb.editor_ids.append(user_id)
2494 elif role == FOLLOWER_ROLE:
2495 hotlist_pb.follower_ids.append(user_id)
2496 try:
2497 self.hotlists_id_by_user[user_id].append(hotlist_pb.hotlist_id)
2498 except KeyError:
2499 self.hotlists_id_by_user[user_id] = [hotlist_pb.hotlist_id]
2500
2501 def CheckHotlistName(self, cnxn, name, owner_ids):
2502 if not framework_bizobj.IsValidHotlistName(name):
2503 raise exceptions.InputException(
2504 '%s is not a valid name for a Hotlist' % name)
2505 if self.LookupHotlistIDs(cnxn, [name], owner_ids):
2506 raise features_svc.HotlistAlreadyExists()
2507
2508 def CreateHotlist(
2509 self, _cnxn, hotlist_name, summary, description, owner_ids, editor_ids,
2510 issue_ids=None, is_private=None, default_col_spec=None, ts=None):
2511 """Create and store a Hotlist with the given attributes."""
2512 if not framework_bizobj.IsValidHotlistName(hotlist_name):
2513 raise exceptions.InputException()
2514 if not owner_ids: # Should never happen.
2515 raise features_svc.UnownedHotlistException()
2516 if (hotlist_name, owner_ids[0]) in self.test_hotlists:
2517 raise features_svc.HotlistAlreadyExists()
2518 hotlist_item_fields = [
2519 (issue_id, rank*100, owner_ids[0] or None, ts, '') for
2520 rank, issue_id in enumerate(issue_ids or [])]
2521 return self.TestAddHotlist(hotlist_name, summary=summary,
2522 owner_ids=owner_ids, editor_ids=editor_ids,
2523 description=description, is_private=is_private,
2524 hotlist_item_fields=hotlist_item_fields,
2525 default_col_spec=default_col_spec)
2526
2527 def UpdateHotlist(
2528 self, cnxn, hotlist_id, name=None, summary=None, description=None,
2529 is_private=None, default_col_spec=None, owner_id=None,
2530 add_editor_ids=None):
2531 hotlist = self.hotlists_by_id.get(hotlist_id)
2532 if not hotlist:
2533 raise features_svc.NoSuchHotlistException(
2534 'Hotlist "%s" not found!' % hotlist_id)
2535
2536 if owner_id:
2537 old_owner_id = hotlist.owner_ids[0]
2538 self.test_hotlists.pop((hotlist.name, old_owner_id), None)
2539 self.test_hotlists[(hotlist.name, owner_id)] = hotlist
2540
2541 if add_editor_ids:
2542 for editor_id in add_editor_ids:
2543 self.hotlists_id_by_user.get(editor_id, []).append(hotlist_id)
2544
2545 if name is not None:
2546 hotlist.name = name
2547 if summary is not None:
2548 hotlist.summary = summary
2549 if description is not None:
2550 hotlist.description = description
2551 if is_private is not None:
2552 hotlist.is_private = is_private
2553 if default_col_spec is not None:
2554 hotlist.default_col_spec = default_col_spec
2555 if owner_id is not None:
2556 hotlist.owner_ids = [owner_id]
2557 if add_editor_ids:
2558 hotlist.editor_ids.extend(add_editor_ids)
2559
2560 def RemoveHotlistEditors(self, cnxn, hotlist_id, remove_editor_ids):
2561 hotlist = self.hotlists_by_id.get(hotlist_id)
2562 if not hotlist:
2563 raise features_svc.NoSuchHotlistException(
2564 'Hotlist "%s" not found!' % hotlist_id)
2565 for editor_id in remove_editor_ids:
2566 hotlist.editor_ids.remove(editor_id)
2567 self.hotlists_id_by_user[editor_id].remove(hotlist_id)
2568
2569 def AddIssuesToHotlists(self, cnxn, hotlist_ids, added_tuples, issue_svc,
2570 chart_svc, commit=True):
2571 for hotlist_id in hotlist_ids:
2572 self.UpdateHotlistItems(cnxn, hotlist_id, [], added_tuples, commit=commit)
2573
2574 def RemoveIssuesFromHotlists(self, cnxn, hotlist_ids, issue_ids, issue_svc,
2575 chart_svc, commit=True):
2576 for hotlist_id in hotlist_ids:
2577 self.UpdateHotlistItems(cnxn, hotlist_id, issue_ids, [], commit=commit)
2578
2579 def UpdateHotlistIssues(
2580 self,
2581 cnxn,
2582 hotlist_id,
2583 updated_items,
2584 remove_issue_ids,
2585 issue_svc,
2586 chart_svc,
2587 commit=True):
2588 if not updated_items and not remove_issue_ids:
2589 raise exceptions.InputException('No changes to make')
2590
2591 hotlist = self.hotlists_by_id.get(hotlist_id)
2592 if not hotlist:
2593 raise NoSuchHotlistException()
2594
2595 updated_ids = [item.issue_id for item in updated_items]
2596 items = [
2597 item for item in hotlist.items
2598 if item.issue_id not in updated_ids + remove_issue_ids
2599 ]
2600 hotlist.items = sorted(updated_items + items, key=lambda item: item.rank)
2601
2602 # Remove all removed and updated issues.
2603 for issue_id in remove_issue_ids + updated_ids:
2604 try:
2605 self.hotlists_id_by_issue[issue_id].remove(hotlist_id)
2606 except (ValueError, KeyError):
2607 pass
2608 # Add all new or updated issues.
2609 for item in updated_items:
2610 self.hotlists_id_by_issue.setdefault(item.issue_id, []).append(hotlist_id)
2611
2612 def UpdateHotlistItems(
2613 self, cnxn, hotlist_id, remove, added_issue_tuples, commit=True):
2614 hotlist = self.hotlists_by_id.get(hotlist_id)
2615 if not hotlist:
2616 raise features_svc.NoSuchHotlistException(
2617 'Hotlist "%s" not found!' % hotlist_id)
2618 current_issues_ids = {
2619 item.issue_id for item in hotlist.items}
2620 items = [
2621 item for item in hotlist.items if
2622 item.issue_id not in remove]
2623
2624 if hotlist.items:
2625 items_sorted = sorted(hotlist.items, key=lambda item: item.rank)
2626 rank_base = items_sorted[-1].rank + 10
2627 else:
2628 rank_base = 1
2629
2630 new_hotlist_items = [
2631 features_pb2.MakeHotlistItem(
2632 issue_id, rank+rank_base*10, adder_id, date, note)
2633 for rank, (issue_id, adder_id, date, note) in
2634 enumerate(added_issue_tuples)
2635 if issue_id not in current_issues_ids]
2636 items.extend(new_hotlist_items)
2637 hotlist.items = items
2638
2639 for issue_id in remove:
2640 try:
2641 self.hotlists_id_by_issue[issue_id].remove(hotlist_id)
2642 except ValueError:
2643 pass
2644 for item in new_hotlist_items:
2645 try:
2646 self.hotlists_id_by_issue[item.issue_id].append(hotlist_id)
2647 except KeyError:
2648 self.hotlists_id_by_issue[item.issue_id] = [hotlist_id]
2649
2650 def UpdateHotlistItemsFields(
2651 self, cnxn, hotlist_id, new_ranks=None, new_notes=None, commit=True):
2652 hotlist = self.hotlists_by_id.get(hotlist_id)
2653 if not hotlist:
2654 raise features_svc.NoSuchHotlistException(
2655 'Hotlist "%s" not found!' % hotlist_id)
2656 if new_ranks is None:
2657 new_ranks = {}
2658 if new_notes is None:
2659 new_notes = {}
2660 for hotlist_item in hotlist.items:
2661 if hotlist_item.issue_id in new_ranks:
2662 hotlist_item.rank = new_ranks[hotlist_item.issue_id]
2663 if hotlist_item.issue_id in new_notes:
2664 hotlist_item.note = new_notes[hotlist_item.issue_id]
2665
2666 hotlist.items.sort(key=lambda item: item.rank)
2667
2668 def TransferHotlistOwnership(
2669 self, cnxn, hotlist, new_owner_id, remain_editor, commit=True):
2670 """Transfers ownership of a hotlist to a new owner."""
2671 new_editor_ids = hotlist.editor_ids
2672 if remain_editor:
2673 new_editor_ids.extend(hotlist.owner_ids)
2674 if new_owner_id in new_editor_ids:
2675 new_editor_ids.remove(new_owner_id)
2676 new_follower_ids = hotlist.follower_ids
2677 if new_owner_id in new_follower_ids:
2678 new_follower_ids.remove(new_owner_id)
2679 self.UpdateHotlistRoles(
2680 cnxn, hotlist.hotlist_id, [new_owner_id], new_editor_ids,
2681 new_follower_ids, commit=commit)
2682
2683 def LookupUserHotlists(self, cnxn, user_ids):
2684 """Return dict of {user_id: [hotlist_id, hotlist_id...]}."""
2685 users_hotlists_dict = {
2686 user_id: self.hotlists_id_by_user.get(user_id, [])
2687 for user_id in user_ids
2688 }
2689 return users_hotlists_dict
2690
2691 def LookupIssueHotlists(self, cnxn, issue_ids):
2692 """Return dict of {issue_id: [hotlist_id, hotlist_id...]}."""
2693 issues_hotlists_dict = {
2694 issue_id: self.hotlists_id_by_issue[issue_id]
2695 for issue_id in issue_ids
2696 if issue_id in self.hotlists_id_by_issue}
2697 return issues_hotlists_dict
2698
2699 def LookupHotlistIDs(self, cnxn, hotlist_names, owner_ids):
2700 id_dict = {}
2701 for name in hotlist_names:
2702 for owner_id in owner_ids:
2703 hotlist = self.test_hotlists.get((name, owner_id))
2704 if hotlist:
2705 if not hotlist.owner_ids: # Should never happen.
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01002706 logging.warning(
2707 'Unowned Hotlist: id:%r, name:%r', hotlist.hotlist_id,
2708 hotlist.name)
Copybara854996b2021-09-07 19:36:02 +00002709 continue
2710 id_dict[(name.lower(), owner_id)] = hotlist.hotlist_id
2711 return id_dict
2712
2713 def GetHotlists(self, cnxn, hotlist_ids, use_cache=True):
2714 """Returns dict of {hotlist_id: hotlist PB}."""
2715 result = {}
2716 for hotlist_id in hotlist_ids:
2717 hotlist = self.hotlists_by_id.get(hotlist_id)
2718 if hotlist:
2719 result[hotlist_id] = hotlist
2720 else:
2721 raise features_svc.NoSuchHotlistException()
2722 return result
2723
2724 def GetHotlistsByUserID(self, cnxn, user_id, use_cache=True):
2725 """Get a list of hotlist PBs for a given user."""
2726 hotlist_id_dict = self.LookupUserHotlists(cnxn, [user_id])
2727 hotlists = self.GetHotlists(cnxn, hotlist_id_dict.get(
2728 user_id, []), use_cache=use_cache)
2729 return list(hotlists.values())
2730
2731 def GetHotlistsByIssueID(self, cnxn, issue_id, use_cache=True):
2732 """Get a list of hotlist PBs for a given issue."""
2733 hotlist_id_dict = self.LookupIssueHotlists(cnxn, [issue_id])
2734 hotlists = self.GetHotlists(cnxn, hotlist_id_dict.get(
2735 issue_id, []), use_cache=use_cache)
2736 return list(hotlists.values())
2737
2738 def GetHotlist(self, cnxn, hotlist_id, use_cache=True):
2739 """Return hotlist PB."""
2740 hotlist_id_dict = self.GetHotlists(cnxn, [hotlist_id], use_cache=use_cache)
2741 return hotlist_id_dict.get(hotlist_id)
2742
2743 def GetHotlistsByID(self, cnxn, hotlist_ids, use_cache=True):
2744 hotlists_dict = {}
2745 missed_ids = []
2746 for hotlist_id in hotlist_ids:
2747 hotlist = self.hotlists_by_id.get(hotlist_id)
2748 if hotlist:
2749 hotlists_dict[hotlist_id] = hotlist
2750 else:
2751 missed_ids.append(hotlist_id)
2752 return hotlists_dict, missed_ids
2753
2754 def GetHotlistByID(self, cnxn, hotlist_id, use_cache=True):
2755 hotlists_dict, _ = self.GetHotlistsByID(
2756 cnxn, [hotlist_id], use_cache=use_cache)
2757 return hotlists_dict[hotlist_id]
2758
2759 def UpdateHotlistRoles(
2760 self, cnxn, hotlist_id, owner_ids, editor_ids, follower_ids, commit=True):
2761 hotlist = self.hotlists_by_id.get(hotlist_id)
2762 if not hotlist:
2763 raise features_svc.NoSuchHotlistException(
2764 'Hotlist "%s" not found!' % hotlist_id)
2765
2766 # Remove hotlist_ids to clear old roles
2767 for user_id in (hotlist.owner_ids + hotlist.editor_ids +
2768 hotlist.follower_ids):
2769 if hotlist_id in self.hotlists_id_by_user[user_id]:
2770 self.hotlists_id_by_user[user_id].remove(hotlist_id)
2771 old_owner_id = None
2772 if hotlist.owner_ids:
2773 old_owner_id = hotlist.owner_ids[0]
2774 self.test_hotlists.pop((hotlist.name, old_owner_id), None)
2775
2776 hotlist.owner_ids = owner_ids
2777 hotlist.editor_ids = editor_ids
2778 hotlist.follower_ids = follower_ids
2779
2780 # Add new hotlist roles
2781 for user_id in owner_ids+editor_ids+follower_ids:
2782 try:
2783 if hotlist_id not in self.hotlists_id_by_user[user_id]:
2784 self.hotlists_id_by_user[user_id].append(hotlist_id)
2785 except KeyError:
2786 self.hotlists_id_by_user[user_id] = [hotlist_id]
2787 new_owner_id = None
2788 if owner_ids:
2789 new_owner_id = owner_ids[0]
2790 self.test_hotlists[(hotlist.name, new_owner_id)] = hotlist
2791
2792 def DeleteHotlist(self, cnxn, hotlist_id, commit=True):
2793 hotlist = self.hotlists_by_id.pop(hotlist_id, None)
2794 if hotlist is not None:
2795 self.test_hotlists.pop((hotlist.name, hotlist.owner_ids[0]), None)
2796 user_ids = hotlist.owner_ids+hotlist.editor_ids+hotlist.follower_ids
2797 for user_id in user_ids:
2798 try:
2799 self.hotlists_id_by_user[user_id].remove(hotlist_id)
2800 except (ValueError, KeyError):
2801 pass
2802 for item in hotlist.items:
2803 try:
2804 self.hotlists_id_by_issue[item.issue_id].remove(hotlist_id)
2805 except (ValueError, KeyError):
2806 pass
2807 for owner_id in hotlist.owner_ids:
2808 self.test_hotlists.pop((hotlist.name, owner_id), None)
2809
2810 def ExpungeHotlists(
2811 self, cnxn, hotlist_ids, star_svc, user_svc, chart_svc, commit=True):
2812 self.expunged_hotlist_ids.extend(hotlist_ids)
2813 for hotlist_id in hotlist_ids:
2814 self.DeleteHotlist(cnxn, hotlist_id)
2815
2816 def ExpungeUsersInHotlists(
2817 self, cnxn, user_ids, star_svc, user_svc, chart_svc):
2818 self.expunged_users_in_hotlists.extend(user_ids)
2819
2820 # end of Hotlist functions
2821
2822 def GetRecentCommands(self, cnxn, user_id, project_id):
2823 return [], []
2824
2825 def ExpungeSavedQueriesExecuteInProject(self, _cnxn, project_id):
2826 self.expunged_saved_queries.append(project_id)
2827
2828 def ExpungeSavedQueriesByUsers(self, cnxn, user_ids, limit=None):
2829 self.expunged_users_in_saved_queries.extend(user_ids)
2830
2831 def ExpungeFilterRules(self, _cnxn, project_id):
2832 self.expunged_filter_rules.append(project_id)
2833
2834 def ExpungeFilterRulesByUser(self, cnxn, user_ids_by_email):
2835 emails = user_ids_by_email.keys()
2836 user_ids = user_ids_by_email.values()
2837 project_rules_dict = collections.defaultdict(list)
Adrià Vilanova Martínezf19ea432024-01-23 20:20:52 +01002838 for project_id, rules in self.test_rules.items():
Copybara854996b2021-09-07 19:36:02 +00002839 for rule in rules:
2840 if rule.default_owner_id in user_ids:
2841 project_rules_dict[project_id].append(rule)
2842 continue
2843 if any(cc_id in user_ids for cc_id in rule.add_cc_ids):
2844 project_rules_dict[project_id].append(rule)
2845 continue
2846 if any(addr in emails for addr in rule.add_notify_addrs):
2847 project_rules_dict[project_id].append(rule)
2848 continue
2849 if any((email in rule.predicate) for email in emails):
2850 project_rules_dict[project_id].append(rule)
2851 continue
2852 self.test_rules[project_id] = [
2853 rule for rule in rules
2854 if rule not in project_rules_dict[project_id]]
2855 return project_rules_dict
2856
2857 def ExpungeQuickEditHistory(self, _cnxn, project_id):
2858 self.expunged_quick_edit.append(project_id)
2859
2860 def ExpungeQuickEditsByUsers(self, cnxn, user_ids, limit=None):
2861 self.expunged_users_in_quick_edits.extend(user_ids)
2862
2863 def GetFilterRules(self, cnxn, project_id):
2864 return self.test_rules[project_id]
2865
2866 def GetCannedQueriesByProjectID(self, cnxn, project_id):
2867 return [sq for (pid, _, sq) in self.saved_queries if pid == project_id]
2868
2869 def GetSavedQueriesByUserID(self, cnxn, user_id):
2870 return [sq for (_, uid, sq) in self.saved_queries if uid == user_id]
2871
2872 def UpdateCannedQueries(self, cnxn, project_id, canned_queries):
2873 self.saved_queries.extend(
2874 [(project_id, None, cq) for cq in canned_queries])
2875
2876 def UpdateUserSavedQueries(self, cnxn, user_id, saved_queries):
2877 self.saved_queries = [
2878 (pid, uid, sq) for (pid, uid, sq) in self.saved_queries
2879 if uid != user_id]
2880 for sq in saved_queries:
2881 if sq.executes_in_project_ids:
2882 self.saved_queries.extend(
2883 [(eipid, user_id, sq) for eipid in sq.executes_in_project_ids])
2884 else:
2885 self.saved_queries.append((None, user_id, sq))
2886
2887 def GetSubscriptionsInProjects(self, cnxn, project_ids):
2888 sq_by_uid = {}
2889 for pid, uid, sq in self.saved_queries:
2890 if pid in project_ids:
2891 if uid in sq_by_uid:
2892 sq_by_uid[uid].append(sq)
2893 else:
2894 sq_by_uid[uid] = [sq]
2895
2896 return sq_by_uid
2897
2898 def GetSavedQuery(self, cnxn, query_id):
2899 return tracker_pb2.SavedQuery()
2900
2901
2902class PostData(object):
2903 """A dictionary-like object that also implements getall()."""
2904
2905 def __init__(self, *args, **kwargs):
2906 self.dictionary = dict(*args, **kwargs)
2907
2908 def getall(self, key):
2909 """Return all values, assume that the value at key is already a list."""
2910 return self.dictionary.get(key, [])
2911
Adrià Vilanova Martínez9f9ade52022-10-10 23:20:11 +02002912 def getlist(self, key):
2913 """Return all values, assume that the value at key is already a list."""
2914 return self.dictionary.get(key, [])
2915
Copybara854996b2021-09-07 19:36:02 +00002916 def get(self, key, default=None):
2917 """Return first value, assume that the value at key is already a list."""
2918 return self.dictionary.get(key, [default])[0]
2919
2920 def __getitem__(self, key):
2921 """Return first value, assume that the value at key is already a list."""
2922 return self.dictionary[key][0]
2923
2924 def __contains__(self, key):
2925 return key in self.dictionary
2926
2927 def keys(self):
2928 """Return the keys in the POST data."""
2929 return list(self.dictionary.keys())
2930
2931
2932class FakeFile:
2933 def __init__(self, data=None):
2934 self.data = data
2935
2936 def read(self):
2937 return self.data
2938
2939 def write(self, content):
2940 return
2941
2942 def __enter__(self):
2943 return self
2944
2945 def __exit__(self, __1, __2, __3):
2946 return None
2947
2948
2949def gcs_open(filename, mode):
2950 return FakeFile(filename)