← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~sinzui/launchpad/person-merge-oopses-0 into lp:launchpad

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/person-merge-oopses-0 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)
Related bugs:
  Bug #779150 in Launchpad itself: "person merge: ProgrammingError: permission denied for relation job"
  https://bugs.launchpad.net/launchpad/+bug/779150
  Bug #779490 in Launchpad itself: "person_merge_job AssertionError: from_person still has email addresses."
  https://bugs.launchpad.net/launchpad/+bug/779490

For more details, see:
https://code.launchpad.net/~sinzui/launchpad/person-merge-oopses-0/+merge/60521

Fix permission and assertion oopses in PersonMergeJob.

    Launchpad bug:
        https://bugs.launchpad.net/bugs/777915
        https://bugs.launchpad.net/bugs/779150
        https://bugs.launchpad.net/bugs/779490
    Pre-implementation: no one

Bug 777915 and bug 779150 show two tables that PersonMergeJob needs access
to. Bug 779490 is caused by a fault in the setContactAddress code that
wrongly preserves purged mailing list email address.

--------------------------------------------------------------------

RULES

    * Run TestPersonSetMerge as the same db user as PersonMergeJob to ensure
      all permissions are exercised.
    * Use MailingListStatus.PURGED to ensure all deletable addresses are
      found.


QA

    * Delete a team with a recipe
    * Delete a team with a milestone structural subscription
    * Delete a team with members
    * Delete a team with an historic list address (~libbls-dev-list)


LINT

    database/schema/security.cfg
    lib/lp/registry/model/person.py
    lib/lp/registry/tests/test_person.py
    lib/lp/registry/tests/test_team.py

^ The new linter see unused variables and white space issues. I can fix these
after the review.


TEST

    ./bin/test -vv -t TestPersonSetMerge -t TestTeamContactAddress


IMPLEMENTATION

Updated the TestPersonSetMerge testcase to do the merge as the same db user
as IPersonMergeJob. Added some helper functions to re-get the objects that
were invalidated by the store change. Several tests were broken, notably those
related to recipes, subscriptions, and membership changes. I updated
security.cfg to make the broken tests pass.
    database/schema/security.cfg
    lib/lp/registry/model/person.py
    lib/lp/registry/tests/test_person.py

Updated setContactAddress to exclude purged mailing lists when building the
tuple of addresses to keep.
    lib/lp/registry/model/person.py
    lib/lp/registry/tests/test_team.py
-- 
https://code.launchpad.net/~sinzui/launchpad/person-merge-oopses-0/+merge/60521
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/person-merge-oopses-0 into lp:launchpad.
=== modified file 'database/schema/security.cfg'
--- database/schema/security.cfg	2011-05-10 04:01:13 +0000
+++ database/schema/security.cfg	2011-05-10 15:36:40 +0000
@@ -3,7 +3,7 @@
 #
 # Possible permissions: SELECT, INSERT, UPDATE, EXECUTE
 #
-# Note that we can't have INSERT only tables if we are using SQLObject, as it
+# Note that we cannot have INSERT only tables if we are using SQLObject, as it
 # creates new entries by first doing an insert (to get the id) and then
 # issuing an update
 [DEFAULT]
@@ -1988,7 +1988,7 @@
 public.distroarchseries                 = SELECT, UPDATE
 public.distrocomponentuploader          = SELECT, UPDATE
 public.distroseries                     = SELECT, UPDATE
-public.emailaddress                     = SELECT, UPDATE
+public.emailaddress                     = SELECT, UPDATE, DELETE
 public.entitlement                      = SELECT, UPDATE
 public.faq                              = SELECT, UPDATE
 public.featureflagchangelogentry        = SELECT, UPDATE
@@ -1996,9 +1996,10 @@
 public.hwsubmission                     = SELECT, UPDATE
 public.ircid                            = SELECT, UPDATE
 public.jabberid                         = SELECT, UPDATE
-public.job                              = SELECT, UPDATE
+public.job                              = SELECT, UPDATE, INSERT
 public.karma                            = SELECT, UPDATE
 public.karmacache                       = SELECT, DELETE
+public.karmacategory                    = SELECT, DELETE
 public.karmatotalcache                  = SELECT, UPDATE, DELETE
 public.logintoken                       = SELECT, UPDATE
 public.mailinglist                      = SELECT, UPDATE
@@ -2007,6 +2008,7 @@
 public.mentoringoffer                   = SELECT, UPDATE
 public.message                          = SELECT, UPDATE
 public.messageapproval                  = SELECT, UPDATE
+public.milestone                        = SELECT, UPDATE
 public.mirror                           = SELECT, UPDATE
 public.nameblacklist                    = SELECT, UPDATE
 public.oauthaccesstoken                 = SELECT, UPDATE
@@ -2023,7 +2025,7 @@
 public.personlocation                   = SELECT, UPDATE
 public.personnotification               = SELECT, INSERT, UPDATE
 public.personsettings                   = SELECT, UPDATE
-public.persontransferjob                = SELECT, UPDATE
+public.persontransferjob                = SELECT, INSERT, UPDATE
 public.pocomment                        = SELECT, UPDATE
 public.poexportrequest                  = SELECT, UPDATE, DELETE
 public.pofile                           = SELECT, UPDATE
@@ -2044,9 +2046,12 @@
 public.revisionauthor                   = SELECT, UPDATE
 public.seriessourcepackagebranch        = SELECT, UPDATE
 public.signedcodeofconduct              = SELECT, UPDATE
+public.sourcepackagename                = SELECT
 public.sourcepackagepublishinghistory   = SELECT, UPDATE
 public.sourcepackagerecipe              = SELECT, UPDATE
 public.sourcepackagerecipebuild         = SELECT, UPDATE
+public.sourcepackagerecipedata          = SELECT, UPDATE
+public.sourcepackagerecipedatainstruction = SELECT, UPDATE
 public.sourcepackagerelease             = SELECT, UPDATE
 public.specification                    = SELECT, UPDATE
 public.specificationbranch              = SELECT, UPDATE

=== modified file 'lib/lp/registry/model/person.py'
--- lib/lp/registry/model/person.py	2011-05-04 07:48:37 +0000
+++ lib/lp/registry/model/person.py	2011-05-10 15:36:40 +0000
@@ -2427,7 +2427,8 @@
             self._setPreferredEmail(email)
         # A team can have up to two addresses, the preferred one and one used
         # by the team mailing list.
-        if self.mailing_list is not None:
+        if (self.mailing_list is not None
+            and self.mailing_list.status != MailingListStatus.PURGED):
             mailing_list_email = getUtility(IEmailAddressSet).getByEmail(
                 self.mailing_list.address)
             if mailing_list_email is not None:

=== modified file 'lib/lp/registry/tests/test_person.py'
--- lib/lp/registry/tests/test_person.py	2011-05-04 07:48:37 +0000
+++ lib/lp/registry/tests/test_person.py	2011-05-10 15:36:40 +0000
@@ -88,6 +88,22 @@
 from lp.testing.views import create_initialized_view
 
 
+def reload_object(obj):
+    """Return a new instance of a storm objet from the store."""
+    store = IStore(Person)
+    return store.get(removeSecurityProxy(obj).__class__, obj.id)
+
+
+def reload_dsp(dsp):
+    """Return a new instance of a DistributionSourcePackage from the store."""
+    store = IStore(Person)
+    distribution_class = removeSecurityProxy(dsp.distribution.__class__)
+    distribution = store.get(distribution_class, dsp.distribution.id)
+    spn_class = removeSecurityProxy(dsp.sourcepackagename.__class__)
+    spn = store.get(spn_class, dsp.sourcepackagename.id)
+    return distribution.getSourcePackage(name=spn.name)
+
+
 class TestPersonTeams(TestCaseWithFactory):
 
     layer = DatabaseFunctionalLayer
@@ -145,7 +161,7 @@
         d_team = self.factory.makeTeam(name='d', owner=self.b_team)
         e_team = self.factory.makeTeam(name='e')
         f_team = self.factory.makeTeam(name='f', owner=e_team)
-        unrelated_team = self.factory.makeTeam(name='unrelated')
+        self.factory.makeTeam(name='unrelated')
         login_person(self.a_team.teamowner)
         d_team.addMember(self.user, d_team.teamowner)
         login_person(e_team.teamowner)
@@ -672,6 +688,17 @@
         transaction.commit()
         logout()
 
+    def _do_merge(self, from_person, to_person, reviewer=None):
+        # Perform the merge as the db user that will be used by the jobs.
+        transaction.commit()
+        reconnect_stores('IPersonMergeJobSource')
+        from_person = reload_object(from_person)
+        to_person = reload_object(to_person)
+        if reviewer is not None:
+            reviewer = reload_object(reviewer)
+        self.person_set.merge(from_person, to_person, reviewer=reviewer)
+        return from_person, to_person
+
     def _get_testable_account(self, person, date_created, openid_identifier):
         # Return a naked account with predictable attributes.
         account = removeSecurityProxy(person.account)
@@ -689,7 +716,7 @@
             person.account).openid_identifiers.any().identifier
         self._do_premerge(duplicate, person)
         login_person(person)
-        self.person_set.merge(duplicate, person)
+        duplicate, person = self._do_merge(duplicate, person)
         self.assertEqual(
             0,
             removeSecurityProxy(duplicate.account).openid_identifiers.count())
@@ -715,7 +742,7 @@
         person = self.factory.makePerson()
         self._do_premerge(duplicate, person)
         login_person(person)
-        self.person_set.merge(duplicate, person)
+        duplicate, person = self._do_merge(duplicate, person)
         self.assertEqual([], duplicate.karma_category_caches)
         self.assertEqual(0, duplicate.karma)
         self.assertEqual(15, person.karma)
@@ -738,7 +765,7 @@
         person = self.person_set.get(person.id)
         self._do_premerge(duplicate, person)
         login_person(person)
-        self.person_set.merge(duplicate, person)
+        duplicate, person = self._do_merge(duplicate, person)
         self.assertEqual([], duplicate.karma_category_caches)
         self.assertEqual(0, duplicate.karma)
         self.assertEqual(28, person.karma)
@@ -752,14 +779,14 @@
         removeSecurityProxy(duplicate).datecreated = oldest_date
         self._do_premerge(duplicate, person)
         login_person(person)
-        self.person_set.merge(duplicate, person)
+        duplicate, person = self._do_merge(duplicate, person)
         self.assertEqual(oldest_date, person.datecreated)
 
     def test_team_with_active_mailing_list_raises_error(self):
         # A team with an active mailing list cannot be merged.
         target_team = self.factory.makeTeam()
         test_team = self.factory.makeTeam()
-        mailing_list = self.factory.makeMailingList(
+        self.factory.makeMailingList(
             test_team, test_team.teamowner)
         self.assertRaises(
             AssertionError, self.person_set.merge, test_team, target_team)
@@ -772,9 +799,11 @@
             test_team, test_team.teamowner)
         mailing_list.deactivate()
         mailing_list.transitionToStatus(MailingListStatus.INACTIVE)
-        self.person_set.merge(test_team, target_team, test_team.teamowner)
+        test_team, target_team = self._do_merge(
+            test_team, target_team, test_team.teamowner)
         self.assertEqual(target_team, test_team.merged)
-        self.assertEqual(MailingListStatus.PURGED, mailing_list.status)
+        self.assertEqual(
+            MailingListStatus.PURGED, test_team.mailing_list.status)
         emails = getUtility(IEmailAddressSet).getByPerson(target_team).count()
         self.assertEqual(0, emails)
 
@@ -787,7 +816,8 @@
         mailing_list.deactivate()
         mailing_list.transitionToStatus(MailingListStatus.INACTIVE)
         mailing_list.purge()
-        self.person_set.merge(test_team, target_team, test_team.teamowner)
+        test_team, target_team = self._do_merge(
+            test_team, target_team, test_team.teamowner)
         self.assertEqual(target_team, test_team.merged)
 
     def test_team_with_members(self):
@@ -797,7 +827,8 @@
         former_member = self.factory.makePerson()
         with person_logged_in(test_team.teamowner):
             test_team.addMember(former_member, test_team.teamowner)
-        self.person_set.merge(test_team, target_team, test_team.teamowner)
+        test_team, target_team = self._do_merge(
+            test_team, target_team, test_team.teamowner)
         self.assertEqual(target_team, test_team.merged)
         self.assertEqual([], list(former_member.super_teams))
 
@@ -807,7 +838,7 @@
         test_team = self.factory.makeTeam()
         target_team = self.factory.makeTeam()
         login_person(test_team.teamowner)
-        self.person_set.merge(test_team, target_team, test_team.teamowner)
+        self._do_merge(test_team, target_team, test_team.teamowner)
 
     def test_team_with_super_teams(self):
         # A team with superteams can be merged, but the memberships
@@ -817,7 +848,8 @@
         target_team = self.factory.makeTeam()
         login_person(test_team.teamowner)
         test_team.join(super_team, test_team.teamowner)
-        self.person_set.merge(test_team, target_team, test_team.teamowner)
+        test_team, target_team = self._do_merge(
+            test_team, target_team, test_team.teamowner)
         self.assertEqual(target_team, test_team.merged)
         self.assertEqual([], list(target_team.super_teams))
 
@@ -826,9 +858,10 @@
         # are moved.
         person = self.factory.makePerson()
         branch = self.factory.makeBranch()
+        duplicate = branch.owner
         self._do_premerge(branch.owner, person)
         login_person(person)
-        self.person_set.merge(branch.owner, person)
+        duplicate, person = self._do_merge(duplicate, person)
         branches = person.getBranches()
         self.assertEqual(1, branches.count())
 
@@ -839,9 +872,10 @@
         from_branch = self.factory.makeBranch(name='foo', product=product)
         to_branch = self.factory.makeBranch(name='foo', product=product)
         mergee = to_branch.owner
-        self._do_premerge(from_branch.owner, mergee)
+        duplicate = from_branch.owner
+        self._do_premerge(duplicate, mergee)
         login_person(mergee)
-        self.person_set.merge(from_branch.owner, mergee)
+        duplicate, mergee = self._do_merge(duplicate, mergee)
         branches = [b.name for b in mergee.getBranches()]
         self.assertEqual(2, len(branches))
         self.assertEqual([u'foo', u'foo-1'], branches)
@@ -851,12 +885,13 @@
         # moved.
         person = self.factory.makePerson()
         recipe = self.factory.makeSourcePackageRecipe()
+        duplicate = recipe.owner
         # Delete the PPA, which is required for the merge to work.
-        with person_logged_in(recipe.owner):
+        with person_logged_in(duplicate):
             recipe.owner.archive.status = ArchiveStatus.DELETED
-        self._do_premerge(recipe.owner, person)
+        self._do_premerge(duplicate, person)
         login_person(person)
-        self.person_set.merge(recipe.owner, person)
+        duplicate, person = self._do_merge(duplicate, person)
         self.assertEqual(1, person.recipes.count())
 
     def test_merge_with_duplicated_recipes(self):
@@ -866,20 +901,21 @@
             name=u'foo', description=u'FROM')
         merge_to = self.factory.makeSourcePackageRecipe(
             name=u'foo', description=u'TO')
+        duplicate = merge_from.owner
         mergee = merge_to.owner
         # Delete merge_from's PPA, which is required for the merge to work.
         with person_logged_in(merge_from.owner):
             merge_from.owner.archive.status = ArchiveStatus.DELETED
         self._do_premerge(merge_from.owner, mergee)
         login_person(mergee)
-        self.person_set.merge(merge_from.owner, merge_to.owner)
+        duplicate, mergee = self._do_merge(duplicate, mergee)
         recipes = mergee.recipes
         self.assertEqual(2, recipes.count())
         descriptions = [r.description for r in recipes]
         self.assertEqual([u'TO', u'FROM'], descriptions)
         self.assertEqual(u'foo-1', recipes[1].name)
 
-    def assertSubscriptionMerges(self, target):
+    def assertSubscriptionMerges(self, target, reloader=reload_object):
         # Given a subscription target, we want to make sure that subscriptions
         # that the duplicate person made are carried over to the merged
         # account.
@@ -889,13 +925,15 @@
         person = self.factory.makePerson()
         self._do_premerge(duplicate, person)
         login_person(person)
-        self.person_set.merge(duplicate, person)
+        duplicate, person = self._do_merge(duplicate, person)
         # The merged person has the subscription, and the duplicate person
         # does not.
+        target = reloader(target)
         self.assertTrue(target.getSubscription(person) is not None)
         self.assertTrue(target.getSubscription(duplicate) is None)
 
-    def assertConflictingSubscriptionDeletes(self, target):
+    def assertConflictingSubscriptionDeletes(self, target,
+                                                      reloader=reload_object):
         # Given a subscription target, we want to make sure that subscriptions
         # that the duplicate person made that conflict with existing
         # subscriptions in the merged account are deleted.
@@ -910,7 +948,8 @@
                 u'a marker')
         self._do_premerge(duplicate, person)
         login_person(person)
-        self.person_set.merge(duplicate, person)
+        duplicate, person = self._do_merge(duplicate, person)
+        target = reloader(target)
         # The merged person still has the original subscription, as shown
         # by the marker name.
         self.assertEqual(
@@ -973,13 +1012,13 @@
 
     def test_merge_with_sourcepackage_subscription(self):
         # See comments in assertSubscriptionMerges.
-        self.assertSubscriptionMerges(
-            self.factory.makeDistributionSourcePackage())
+        dsp = self.factory.makeDistributionSourcePackage()
+        self.assertSubscriptionMerges(dsp, reloader=reload_dsp)
 
     def test_merge_with_conflicting_sourcepackage_subscription(self):
         # See comments in assertConflictingSubscriptionDeletes.
-        self.assertConflictingSubscriptionDeletes(
-            self.factory.makeDistributionSourcePackage())
+        dsp = self.factory.makeDistributionSourcePackage()
+        self.assertConflictingSubscriptionDeletes(dsp, reloader=reload_dsp)
 
     def test_mergeAsync(self):
         # mergeAsync() creates a new `PersonMergeJob`.

=== modified file 'lib/lp/registry/tests/test_team.py'
--- lib/lp/registry/tests/test_team.py	2011-04-08 02:51:34 +0000
+++ lib/lp/registry/tests/test_team.py	2011-05-10 15:36:40 +0000
@@ -104,6 +104,16 @@
         self.assertEqual(None, self.team.preferredemail)
         self.assertEqual([list_address], self.getAllEmailAddresses())
 
+    def test_setContactAddress_with_purged_mailing_list_to_none(self):
+        # Purging a mailing list will delete the list address, but this was
+        # not always the case. The address will be deleted if it still exists.
+        list_address = self.createMailingListAndGetAddress()
+        naked_mailing_list = removeSecurityProxy(self.team.mailing_list)
+        naked_mailing_list.status = MailingListStatus.PURGED
+        self.team.setContactAddress(None)
+        self.assertEqual(None, self.team.preferredemail)
+        self.assertEqual([], self.getAllEmailAddresses())
+
     def test_setContactAddress_after_purged_mailing_list_and_rename(self):
         # This is the rare case where a list is purged for a team rename,
         # then the contact address is set/unset sometime afterwards.