Exemplo n.º 1
0
    def testUnicode(self):
        """Test various interesting Unicode characters."""
        base_name = escape.utf8(
            u'ààà朋友你好abc123\U00010000\U00010000\x00\x01\b\n\t ')
        timestamp = time.time()
        contact_id_lookup = dict()

        def _CreateContact(index):
            name = base_name + str(index)
            identity_key = 'Email:%s' % name
            return Contact.CreateFromKeywords(100, [(identity_key, None)],
                                              timestamp,
                                              Contact.GMAIL,
                                              name=name)

        def _VerifyContacts(query_expr, start_key, end_key, exp_indexes):
            actual_contacts = self._RunAsync(Contact.IndexQuery,
                                             self._client,
                                             query_expr,
                                             None,
                                             start_index_key=start_key,
                                             end_index_key=end_key)
            self.assertEqual(len(exp_indexes), len(actual_contacts))
            for expected, actual in zip(
                [_CreateContact(i) for i in exp_indexes], actual_contacts):
                self.assertEqual(expected._asdict(), actual._asdict())

        # Create 3 contacts under user 100 in the db.
        for i in xrange(3):
            contact = _CreateContact(i)
            contact_id_lookup[i] = contact.contact_id
            self._RunAsync(contact.Update, self._client)

        # Get contact by identity.
        identity_key = 'Email:%s' % base_name
        _VerifyContacts(('contact.identities={i}', {
            'i': identity_key + '0'
        }), None, None, [0])

        # Get multiple contacts.
        _VerifyContacts(('contact.identities={i} | contact.identities={i2}', {
            'i': identity_key + '0',
            'i2': identity_key + '1'
        }), None, None, [1, 0])

        # Get contact with start key.
        sort_key = Contact.CreateSortKey(contact_id_lookup[1], timestamp)
        _VerifyContacts(('contact.identities={i} | contact.identities={i2}', {
            'i': identity_key + '0',
            'i2': identity_key + '1'
        }), DBKey(100, sort_key), None, [0])

        # Get contact with end key.
        sort_key = Contact.CreateSortKey(contact_id_lookup[0], timestamp)
        _VerifyContacts(('contact.identities={i} | contact.identities={i2}', {
            'i': identity_key + '0',
            'i2': identity_key + '1'
        }), None, DBKey(100, sort_key), [1])
Exemplo n.º 2
0
 def ValidateContactNotifications(self, name, identity_key, op_dict):
   """Validates that a notification was created for each user who
   references a contact of the specified identity.
   """
   invalidate = {'contacts': {'start_key': Contact.CreateSortKey(None, op_dict['op_timestamp'])}}
   for co in self.QueryModelObjects(Contact, predicate=lambda co: identity_key in co.identities):
     self.ValidateNotification(name, co.user_id, op_dict, invalidate)
Exemplo n.º 3
0
    def _ValidateRemoveOneContact(contact_id):
        predicate = lambda c: c.contact_id == contact_id
        existing_contacts = validator.QueryModelObjects(Contact,
                                                        predicate=predicate,
                                                        query_forward=True)
        if len(existing_contacts) > 0:
            last_contact = existing_contacts[-1]
            if last_contact.IsRemoved():
                # Keep the last matching contact because it's already been removed.
                existing_contacts = existing_contacts[:-1]
            elif not removed_contacts_reset:
                removed_contact_dict = {
                    'user_id':
                    user_id,
                    'contact_id':
                    contact_id,
                    'contact_source':
                    Contact.GetContactSourceFromContactId(contact_id),
                    'timestamp':
                    util._TEST_TIME,
                    'sort_key':
                    Contact.CreateSortKey(contact_id, util._TEST_TIME),
                    'labels': [Contact.REMOVED]
                }
                validator.ValidateCreateContact(identities_properties=None,
                                                **removed_contact_dict)

            for contact in existing_contacts:
                db_key = DBKey(user_id, contact.sort_key)
                validator.ValidateDeleteDBObject(Contact, db_key)
Exemplo n.º 4
0
        def _VerifyAccountStatus(cookie, results):
            u = results['user']
            dev = results['device']
            ident = results['identity']
            self.assertEqual(ident.user_id, u.user_id)
            self.assertTrue(u.name)
            self.assertTrue(u.given_name)
            self.assertTrue(u.family_name)
            self.assertIsNotNone(u.webapp_dev_id)
            [
                self.assertEqual(getattr(dev, k), v)
                for k, v in self._mobile_device_dict.items()
            ]

            # Keep querying until notifications are found.
            while True:
                response_dict = self._SendRequest('query_notifications',
                                                  cookie, {})
                if len(response_dict['notifications']) > 2:
                    break
                time.sleep(0.100)

            self.assertEqual(response_dict['notifications'][1]['name'],
                             'register friend')
            notification = response_dict['notifications'][2]
            self.assertEqual(notification['name'], 'fetch_contacts')
            sort_key = Contact.CreateSortKey(None, notification['timestamp'])
            self.assertEqual(
                notification['invalidate']['contacts']['start_key'], sort_key)
            self.stop()
Exemplo n.º 5
0
    def testUnlinkContacts(self):
        """Test unlinking identity that is referenced by contacts."""
        for i in xrange(3):
            contact_dict = Contact.CreateContactDict(
                self._users[i].user_id, [('FacebookGraph:100', None)],
                util._TEST_TIME,
                Contact.FACEBOOK,
                rank=i)
            self._UpdateOrAllocateDBObject(Contact, **contact_dict)

        self._tester.LinkFacebookUser({'id': 100}, user_cookie=self._cookie)
        response_dict = self._tester.QueryContacts(
            self._cookie2,
            start_key=Contact.CreateSortKey(None, util._TEST_TIME))
        self.assertEqual(len(response_dict['contacts']), 1)

        # Now unlink the identity and make sure contacts are updated.
        util._TEST_TIME += 1
        self._tester.UnlinkIdentity(self._cookie, 'FacebookGraph:100')
        response_dict = self._tester.QueryContacts(
            self._cookie2,
            start_key=Contact.CreateSortKey(None, util._TEST_TIME))
        self.assertEqual(len(response_dict['contacts']), 1)

        # Now repeat, but with older contacts.
        util._TEST_TIME += 1
        timestamp = time.time() - 100
        for i in xrange(3):
            contact_dict = Contact.CreateContactDict(
                self._users[i].user_id, [('FacebookGraph:101', None)],
                timestamp,
                Contact.FACEBOOK,
                rank=i)
            contact = self._UpdateOrAllocateDBObject(Contact, **contact_dict)

        self._tester.LinkFacebookUser({'id': 101}, user_cookie=self._cookie)
        self._tester.UnlinkIdentity(self._cookie, 'FacebookGraph:101')
        response_dict = self._tester.QueryContacts(
            self._cookie3,
            start_key=Contact.CreateSortKey(None, util._TEST_TIME))
        self.assertEqual(len(response_dict['contacts']), 1)
Exemplo n.º 6
0
 def NotifyUploadContacts(cls, client, user_id, timestamp):
     """Adds a notification for the specified user that new contacts have been uploaded/updated and need
 to be pulled to the client. Invalidates all the newly uploaded/updated contacts, which all have
 sort_key greater than or equal to "timestamp".
 """
     invalidate = {
         'contacts': {
             'start_key': Contact.CreateSortKey(None, timestamp)
         }
     }
     yield NotificationManager.CreateForUser(client,
                                             user_id,
                                             'upload_contacts',
                                             invalidate=invalidate)
Exemplo n.º 7
0
  def ValidateRewriteContacts(self, identity_key, op_dict):
    """Validates that all contacts that reference "identity_key" have been updated with the
    new timestamp.
    """
    # Iterate over all contacts that reference the identity.
    for co in self.QueryModelObjects(Contact, predicate=lambda co: identity_key in co.identities):
      # Validate that contact has been updated.
      contact_dict = co._asdict()
      contact_dict['timestamp'] = op_dict['op_timestamp']
      sort_key = Contact.CreateSortKey(Contact.CalculateContactId(contact_dict), contact_dict['timestamp'])
      contact_dict['sort_key'] = sort_key
      self.ValidateUpdateDBObject(Contact, **contact_dict)

      # Validate that any old contacts have been deleted.
      if sort_key != co.sort_key:
        self.ValidateDeleteDBObject(Contact, co.GetKey())
Exemplo n.º 8
0
 def NotifyRemoveContacts(cls, client, user_id, timestamp, reload_all):
     """Adds a notification for the specified user that contacts have been removed and need
 to be removed from the client. Invalidates all the newly removed contacts, which all have
 sort_key greater than or equal to "timestamp".
 """
     invalidate = {
         'contacts': {
             'start_key':
             Contact.CreateSortKey(None, 0 if reload_all else timestamp)
         }
     }
     if reload_all:
         invalidate['contacts']['all'] = True
     yield NotificationManager.CreateForUser(client,
                                             user_id,
                                             'remove_contacts',
                                             invalidate=invalidate)
Exemplo n.º 9
0
 def NotifyFetchContacts(cls, client, user_id, timestamp, reload_all):
     """Adds a notification for the specified user that new contacts have been fetched and need
 to be pulled to the client. Invalidates all the newly fetched contacts, which all have
 sort_key greater than "timestamp".
 """
     invalidate = {
         'contacts': {
             'start_key':
             Contact.CreateSortKey(None, 0 if reload_all else timestamp)
         }
     }
     if reload_all:
         invalidate['contacts']['all'] = True
     yield NotificationManager.CreateForUser(client,
                                             user_id,
                                             'fetch_contacts',
                                             invalidate=invalidate)
Exemplo n.º 10
0
    def _NotifyUsersWithContact(cls, client, name, identity_key, timestamp):
        """Adds a notification to all users having contacts with the specified identity. Invalidates
    all contacts added after the specified timestamp.
    """
        @gen.coroutine
        def _VisitContactUserId(contact_user_id):
            yield NotificationManager.CreateForUser(client,
                                                    contact_user_id,
                                                    name,
                                                    invalidate=invalidate)

        invalidate = {
            'contacts': {
                'start_key': Contact.CreateSortKey(None, timestamp)
            }
        }
        yield gen.Task(Contact.VisitContactUserIds, client, identity_key,
                       _VisitContactUserId)
Exemplo n.º 11
0
  def ValidateCreateContact(self, user_id, identities_properties, timestamp, contact_source, **op_dict):
    """Validates creation of contact along with derived attributes.
    Returns created contact.
    """
    contact_dict = op_dict
    contact_dict['user_id'] = user_id
    contact_dict['timestamp'] = timestamp
    if identities_properties is not None:
      contact_dict['identities_properties'] = identities_properties
      contact_dict['identities'] = set([Identity.Canonicalize(identity_properties[0])
                                        for identity_properties in identities_properties])
    else:
      contact_dict['identities_properties'] = None
    contact_dict['contact_source'] = contact_source
    if 'contact_id' not in contact_dict:
      contact_dict['contact_id'] = Contact.CalculateContactId(contact_dict)
    if 'sort_key' not in contact_dict:
      contact_dict['sort_key'] = Contact.CreateSortKey(contact_dict['contact_id'], timestamp)

    return self.ValidateCreateDBObject(Contact, **contact_dict)
Exemplo n.º 12
0
    def testUnlinkIdentity(self):
        """Verify unlinking an identity causes every referencing contact to be updated."""
        # Create a Peter contact for Spencer.
        timestamp = util.GetCurrentTimestamp()
        spencer = self._user
        contact_identity = 'Email:[email protected]'
        contact_name = 'Peter Mattis'
        contact_given_name = 'Peter'
        contact_family_name = 'Mattis'
        contact_rank = 42
        contact = Contact.CreateFromKeywords(spencer.user_id,
                                             [(contact_identity, None)],
                                             timestamp,
                                             Contact.GMAIL,
                                             name='Peter Mattis',
                                             given_name='Peter',
                                             family_name='Mattis',
                                             rank=42)
        self._RunAsync(contact.Update, self._client)

        peter_ident = self._RunAsync(Identity.Query, self._client,
                                     contact_identity, None)

        # Unlink peter's identity, which should cause Spencer's contact to be updated.
        self._RunAsync(peter_ident.UnlinkIdentity, self._client,
                       self._user2.user_id, contact_identity, timestamp + 1)

        contacts = self._RunAsync(Contact.RangeQuery, self._client,
                                  spencer.user_id, None, None, None)
        self.assertEqual(len(contacts), 1)
        self.assertEqual(
            contacts[0].sort_key,
            Contact.CreateSortKey(contact.contact_id, timestamp + 1))
        self.assertEqual(contacts[0].name, contact_name)
        self.assertEqual(contacts[0].given_name, contact_given_name)
        self.assertEqual(contacts[0].family_name, contact_family_name)
        self.assertEqual(contacts[0].rank, contact_rank)
Exemplo n.º 13
0
def _TestRemoveContacts(tester, user_cookie, request_dict):
    """Called by the ServiceTester in order to test remove_contacts
  service API call.
  """
    def _ValidateRemoveOneContact(contact_id):
        predicate = lambda c: c.contact_id == contact_id
        existing_contacts = validator.QueryModelObjects(Contact,
                                                        predicate=predicate,
                                                        query_forward=True)
        if len(existing_contacts) > 0:
            last_contact = existing_contacts[-1]
            if last_contact.IsRemoved():
                # Keep the last matching contact because it's already been removed.
                existing_contacts = existing_contacts[:-1]
            elif not removed_contacts_reset:
                removed_contact_dict = {
                    'user_id':
                    user_id,
                    'contact_id':
                    contact_id,
                    'contact_source':
                    Contact.GetContactSourceFromContactId(contact_id),
                    'timestamp':
                    util._TEST_TIME,
                    'sort_key':
                    Contact.CreateSortKey(contact_id, util._TEST_TIME),
                    'labels': [Contact.REMOVED]
                }
                validator.ValidateCreateContact(identities_properties=None,
                                                **removed_contact_dict)

            for contact in existing_contacts:
                db_key = DBKey(user_id, contact.sort_key)
                validator.ValidateDeleteDBObject(Contact, db_key)

    validator = tester.validator
    user_id, device_id = tester.GetIdsFromCookie(user_cookie)
    request_dict = deepcopy(request_dict)

    # Send remove_contacts request.
    actual_dict = tester.SendRequest('remove_contacts', user_cookie,
                                     request_dict)
    op_dict = tester._DeriveNotificationOpDict(user_id, device_id,
                                               request_dict)

    # Cheat a little by inspecting the actual notification to see if there was a reset of removed contacts.
    removed_contacts_reset = _CheckForRemovedContactsReset(tester, user_id)
    if removed_contacts_reset:
        # Delete any 'removed' contacts from the model.
        predicate = lambda c: c.IsRemoved()
        removed_contacts = validator.QueryModelObjects(Contact,
                                                       predicate=predicate,
                                                       query_forward=True)
        for removed_contact in removed_contacts:
            validator.ValidateDeleteDBObject(
                Contact, DBKey(user_id, removed_contact.sort_key))

    for contact_id in request_dict['contacts']:
        _ValidateRemoveOneContact(contact_id)

    # Validate that a notification was created for the removal of contacts.
    timestamp = 0 if removed_contacts_reset else op_dict['op_timestamp']
    invalidate = {
        'contacts': {
            'start_key': Contact.CreateSortKey(None, timestamp)
        }
    }
    if removed_contacts_reset:
        invalidate['contacts']['all'] = True
    validator.ValidateNotification('remove_contacts', user_id, op_dict,
                                   invalidate)

    # Increment time so that subsequent contacts will use later time.
    util._TEST_TIME += 1

    tester._CompareResponseDicts('remove_contacts', user_id, request_dict, {},
                                 actual_dict)
    return actual_dict
Exemplo n.º 14
0
  def testSupportMultipleIdentitiesPerContact(self):
    """Ensure that the SUPPORT_MULTIPLE_IDENTITIES_PER_CONTACT migration works correctly."""
    identity_key = 'Email:' + self._user2.email

    # Create an identity to exercise the 'contact_user_id' field of the down-level response.
    self._UpdateOrAllocateDBObject(Identity, key=identity_key, user_id=21)

    # This contact is for a registered user.
    contact_dict = Contact.CreateContactDict(user_id=self._user.user_id,
                                             identities_properties=[(identity_key, 'work'),
                                                                    ('Email:' + self._user3.email, 'home'),
                                                                    ('Phone:+13191234567', 'mobile')],
                                             timestamp=1,
                                             contact_source=Contact.GMAIL,
                                             name='Mike Purtell',
                                             given_name='Mike',
                                             family_name='Purtell',
                                             rank=3)
    self._UpdateOrAllocateDBObject(Contact, **contact_dict)

    # This contact has been removed and shouldn't show up in down-level client queries.
    contact_dict = Contact.CreateContactDict(user_id=self._user.user_id,
                                             identities_properties=None,
                                             timestamp=1,
                                             contact_source=Contact.GMAIL,
                                             contact_id='gm:onetwothree',
                                             sort_key=Contact.CreateSortKey('gm:onetwothree', 1),
                                             labels=[Contact.REMOVED])
    self._UpdateOrAllocateDBObject(Contact, **contact_dict)

    # This contact has unknown identities.
    contact_dict = Contact.CreateContactDict(user_id=self._user.user_id,
                                             identities_properties=[('Email:[email protected]', 'work'),
                                                                    ('Email:[email protected]', 'home'),
                                                                    ('Phone:+13191234567', 'mobile')],
                                             timestamp=1,
                                             contact_source=Contact.GMAIL,
                                             name='Some One',
                                             given_name='Some',
                                             family_name='Body',
                                             rank=5)
    self._UpdateOrAllocateDBObject(Contact, **contact_dict)

    # This contact has just a phone number and shouldn't show up in down-level client queries.
    contact_dict = Contact.CreateContactDict(user_id=self._user.user_id,
                                             identities_properties=[('Phone:+13191234567', 'mobile')],
                                             timestamp=1,
                                             contact_source=Contact.GMAIL,
                                             name='Some One with just a phone',
                                             given_name='Some',
                                             family_name='Body',
                                             rank=42)
    self._UpdateOrAllocateDBObject(Contact, **contact_dict)

    # This contact has no identities and shouldn't show up in down-level client queries.
    contact_dict = Contact.CreateContactDict(user_id=self._user.user_id,
                                             identities_properties=[],
                                             timestamp=1,
                                             contact_source=Contact.GMAIL,
                                             name='Some One with just a phone',
                                             given_name='Some',
                                             family_name='Body',
                                             rank=42)
    self._UpdateOrAllocateDBObject(Contact, **contact_dict)

    response = self._SendRequest('query_contacts', self._cookie, {}, version=Message.SUPPRESS_BLANK_COVER_PHOTO)

    # Verify response is what we expect for down-level client.
    self.assertEqual(response['headers'], {'version': Message.SUPPRESS_BLANK_COVER_PHOTO})
    self.assertEqual(response['num_contacts'], 2)
    response_contact = response['contacts'][1]
    self.assertEqual(response_contact['name'], 'Mike Purtell')
    self.assertEqual(response_contact['given_name'], 'Mike')
    self.assertEqual(response_contact['family_name'], 'Purtell')
    self.assertEqual(response_contact['rank'], 3)
    self.assertEqual(response_contact['contact_user_id'], 21)
    self.assertEqual(response_contact['identity'], identity_key)
    self.assertFalse('identities' in response_contact)
    self.assertFalse('contact_id' in response_contact)
    self.assertFalse('contact_source' in response_contact)
    self.assertFalse('labels' in response_contact)

    response_contact = response['contacts'][0]
    self.assertEqual(response_contact['name'], 'Some One')
    self.assertEqual(response_contact['given_name'], 'Some')
    self.assertEqual(response_contact['family_name'], 'Body')
    self.assertEqual(response_contact['rank'], 5)
    self.assertEqual(response_contact['identity'], 'Email:[email protected]')
    self.assertFalse('contact_user_id' in response_contact)
    self.assertFalse('identities' in response_contact)
    self.assertFalse('contact_id' in response_contact)
    self.assertFalse('contact_source' in response_contact)
    self.assertFalse('labels' in response_contact)
Exemplo n.º 15
0
def _TestUploadContacts(tester, user_cookie, request_dict):
    """Called by the ServiceTester in order to test upload_contacts
  service API call.
  """
    def _ValidateUploadOneContact(contact):
        contact = deepcopy(contact)

        # Transform into proper form for Contact.CalculateContactId()
        identities_properties = [
            (identity_properties['identity'],
             identity_properties.get('description', None))
            for identity_properties in contact['identities']
        ]
        contact.pop('identities')
        contact_dict = Contact.CreateContactDict(user_id,
                                                 identities_properties,
                                                 op_dict['op_timestamp'],
                                                 **contact)
        predicate = lambda c: c.contact_id == contact_dict['contact_id']
        existing_contacts = validator.QueryModelObjects(Contact,
                                                        predicate=predicate)
        # Create contact if it doesn't already exist or it's in a 'removed' state.
        if len(existing_contacts) == 0 or existing_contacts[-1].IsRemoved():
            if len(existing_contacts) != 0:
                # Delete the 'removed' contact.
                validator.ValidateDeleteDBObject(
                    Contact, DBKey(user_id, existing_contacts[-1].sort_key))
            validator.ValidateCreateContact(**contact_dict)
        return contact_dict['contact_id']

    validator = tester.validator
    user_id, device_id = tester.GetIdsFromCookie(user_cookie)
    request_dict = deepcopy(request_dict)

    # Send upload_episode request.
    actual_dict = tester.SendRequest('upload_contacts', user_cookie,
                                     request_dict)
    op_dict = tester._DeriveNotificationOpDict(user_id, device_id,
                                               request_dict)

    result_contact_ids = []
    for contact in request_dict['contacts']:
        contact_id = _ValidateUploadOneContact(contact)
        result_contact_ids.append(contact_id)

    # Validate that a notification was created for the upload of contacts.
    invalidate = {
        'contacts': {
            'start_key': Contact.CreateSortKey(None, op_dict['op_timestamp'])
        }
    }
    validator.ValidateNotification('upload_contacts', user_id, op_dict,
                                   invalidate)

    # Increment time so that subsequent contacts will use later time.
    util._TEST_TIME += 1

    tester._CompareResponseDicts('upload_contacts', user_id, request_dict,
                                 {'contact_ids': result_contact_ids},
                                 actual_dict)
    return actual_dict
Exemplo n.º 16
0
def _ValidateAuthUser(tester, action, user_dict, ident_dict, device_dict, user_cookie, auth_response):
  """Validates an auth action that has taken place and resulted in the HTTP response given
  by "auth_response".
  """
  validator = tester.validator

  # Validate the response from a GET (device_dict is None) or POST to auth service.
  if device_dict is None:
    # Get the id of the user that should have been created by the registration.
    actual_identity = tester._RunAsync(Identity.Query, validator.client, ident_dict['key'], None)
    actual_user_id = actual_identity.user_id
  else:
    # Extract the user_id and device_id from the JSON response.
    response_dict = json.loads(auth_response.body)
    actual_op_id = response_dict['headers']['op_id']
    actual_user_id = response_dict['user_id']
    actual_device_id = response_dict.get('device_id', None)

  # Verify that the cookie in the response contains the correct information.
  cookie_user_dict = tester.DecodeUserCookie(tester.GetCookieFromResponse(auth_response))
  assert cookie_user_dict['user_id'] == actual_user_id, (cookie_user_dict, actual_user_id)
  assert device_dict is None or 'device_id' not in device_dict or \
         cookie_user_dict['device_id'] == device_dict['device_id'], \
         (cookie_user_dict, device_dict)

  actual_user = tester._RunAsync(User.Query, validator.client, actual_user_id, None)
  if device_dict is None:
    # If no mobile device was used, then web device id is expected.
    actual_device_id = actual_user.webapp_dev_id

  # Get notifications that were created. There could be up to 2: a register_user notification and
  # a fetch_contacts notification (in link case).
  notification_list = tester._RunAsync(Notification.RangeQuery,
                                       tester.validator.client,
                                       actual_user_id,
                                       range_desc=None,
                                       limit=3,
                                       col_names=None,
                                       scan_forward=False)
  if device_dict is None:
    actual_op_id = notification_list[1 if action == 'link' else 0].op_id

  # Determine what the registered user's id should have been.
  if user_cookie is None or action != 'link':
    expected_user_id = None
  else:
    expected_user_id, device_id = tester.GetIdsFromCookie(user_cookie)

  expected_identity = validator.GetModelObject(Identity, ident_dict['key'], must_exist=False)
  if expected_identity is not None:
    # Identity already existed, so expect registered user's id to equal the user id of that identity.
    expected_user_id = expected_identity.user_id

  # Verify that identity is linked to expected user.
  assert expected_user_id is None or expected_user_id == actual_user_id, \
         (expected_user_id, actual_user_id)

  # Validate the device if it should have been created.
  if device_dict is None:
    expected_device_dict = None
  else:
    expected_device_dict = deepcopy(device_dict)
    if 'device_id' not in device_dict:
      expected_device_dict['device_id'] = actual_device_id

  # Re-map picture element for Facebook authority (Facebook changed format in Oct 2012).
  scratch_user_dict = deepcopy(user_dict)
  if ident_dict['authority'] == 'Facebook':
    if device_dict is None:
      scratch_user_dict['session_expires'] = ['3600']
    if 'picture' in scratch_user_dict:
      scratch_user_dict['picture'] = scratch_user_dict['picture']['data']['url']
  elif ident_dict['authority'] == 'Viewfinder' and action != 'register':
    # Only use name in registration case.
    scratch_user_dict.pop('name', None)

  # Validate the Identity object.
  expected_ident_dict = deepcopy(ident_dict)
  expected_ident_dict.pop('json_attrs', None)
  if ident_dict['authority'] == 'Viewfinder':
    identity = tester._RunAsync(Identity.Query, tester.validator.client, ident_dict['key'], None)
    expected_ident_dict['access_token'] = identity.access_token
    expected_ident_dict['expires'] = identity.expires

  # Validate the User object.
  expected_user_dict = {}
  before_user = validator.GetModelObject(User, actual_user_id, must_exist=False)
  before_user_dict = {} if before_user is None else before_user._asdict()
  for k, v in scratch_user_dict.items():
    user_key = auth.AuthHandler._AUTH_ATTRIBUTE_MAP.get(k, None)
    if user_key is not None:
      if before_user is None or getattr(before_user, user_key) is None:
        expected_user_dict[auth.AuthHandler._AUTH_ATTRIBUTE_MAP[k]] = v

      # Set facebook email if it has not yet been set.
      if user_key == 'email' and ident_dict['authority'] == 'Facebook':
        if before_user is None or getattr(before_user, 'facebook_email') is None:
          expected_user_dict['facebook_email'] = v


  expected_user_dict['user_id'] = actual_user_id
  expected_user_dict['webapp_dev_id'] = actual_user.webapp_dev_id

  op_dict = {'op_timestamp': util._TEST_TIME,
             'op_id': notification_list[1 if action == 'link' else 0].op_id,
             'user_id': actual_user_id,
             'device_id': actual_device_id}

  if expected_device_dict:
    expected_device_dict.pop('device_uuid', None)
    expected_device_dict.pop('test_udid', None)

  is_prospective = before_user is None or not before_user.IsRegistered()
  validator.ValidateUpdateUser('first register contact' if is_prospective else 'link contact',
                               op_dict,
                               expected_user_dict,
                               expected_ident_dict,
                               device_dict=expected_device_dict)
  after_user_dict = validator.GetModelObject(User, actual_user_id)._asdict()

  if expected_identity is not None:
    expected_ident_dict['user_id'] = expected_identity.user_id
  if action == 'link':
    ignored_keys = ['user_id', 'webapp_dev_id']
    if 'user_id' not in expected_ident_dict and all(k in ignored_keys for k in expected_user_dict.keys()):
      # Only notify self if it hasn't been done through Friends.
      validator.ValidateUserNotification('register friend self', actual_user_id, op_dict)

    # Validate fetch_contacts notification. 
    op_dict['op_id'] = notification_list[0].op_id
    invalidate = {'contacts': {'start_key': Contact.CreateSortKey(None, util._TEST_TIME)}}
    validator.ValidateNotification('fetch_contacts', actual_user_id, op_dict, invalidate)

  return actual_user, actual_device_id if device_dict is not None else None