← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~sinzui/launchpad/private-bug-5 into lp:launchpad

 

Curtis Hovey has proposed merging lp:~sinzui/launchpad/private-bug-5 into lp:launchpad.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~sinzui/launchpad/private-bug-5/+merge/75614

Bugs created via email for private-bugs-default projects are not private.

    Launchpad bug: https://bugs.launchpad.net/bugs/797697
    Pre-implementation: lifeless, jcsackett

MaloneHandler.process() processes email commands as discreet operations:
BugEmailCommand and AffectsEmailCommand are separate operations which do not
support default-private-bugs. The discreet approach assumed the order of
changes is not important.

BugSet.createBug() takes a holistic approach to where all the parameters are
examined and reconciled before the bug is created using a specific order
of calls.

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

RULES

    * When a new bug is being created, MaloneHandler.process() should be
      setting CreateBugParams. When all the information is gathered it will
      call BugSet.createBug()
      * Launchpad web and API code require the target to create the bug,
        the Affects command will create a bug from the BugParams.
      * Some message processing rules in BugEmailCommand need to move to
        AffectsEmailCommand.
    * Most bug email commands expect an IBug:
      * Update the methods to check for bug or bug_params
      * When there is a bug, continue as normal.
      * When there are bug_params, update them and return early.


QA

    * Send an email to a project with private bugs by default that you
      are the bug supervisor for:
        affects my-project
        tags testing
        status triaged
        importance high
    * Verify the bug was created and it that is private.
    * Send an email to a project with private bugs by default that you
      are the security contact for:
        affects my-project
        security true
    * Verify the bug was created and that it is security related and private.
    * Send an email to a project with private bugs by default that you
      are the security contact for:
        affects my-project
        private false
    * Verify the bug was created and that it is public.


LINT

    lib/lp/bugs/interfaces/bug.py
    lib/lp/bugs/mail/commands.py
    lib/lp/bugs/mail/handler.py
    lib/lp/bugs/mail/tests/test_commands.py
    lib/lp/bugs/model/bug.py
    lib/lp/bugs/tests/test_bug.py
    lib/lp/registry/adapters.py
    lib/lp/registry/configure.zcml
    lib/lp/registry/tests/test_adapters.py


TEST

    ./bin/test -vv lp.bugs.mail.tests.test_commands \
    ./bin/test -vv lp.bugs.mail.tests.test_handler \
    ./bin/test -vv -t bug-emailinterface lp.bugs.mail.tests


IMPLEMENTATION

Updated the most all the bug commands and the first bugtask command to
work with bug_params. The Duplicate and Unsubscribe commands do nothing
since these commands are really for existing bugs. I updated malone handler
loop to reconcile the defects reported by the bug-emailinterface.txt test.
    lib/lp/bugs/mail/commands.py
    lib/lp/bugs/mail/handler.py
    lib/lp/bugs/mail/tests/test_commands.py

Updated the signature of CreateBug() to return the event if the callsite needs
to manage it...because it can be aborted. Added support for CVEs.
    lib/lp/bugs/interfaces/bug.py
    lib/lp/bugs/model/bug.py
    lib/lp/bugs/tests/test_bug.py

Added or registered adapters to get product and dsp.
    lib/lp/registry/adapters.py
    lib/lp/registry/configure.zcml
    lib/lp/registry/tests/test_adapters.py
-- 
https://code.launchpad.net/~sinzui/launchpad/private-bug-5/+merge/75614
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~sinzui/launchpad/private-bug-5 into lp:launchpad.
=== modified file 'lib/lp/bugs/interfaces/bug.py'
--- lib/lp/bugs/interfaces/bug.py	2011-09-11 12:50:33 +0000
+++ lib/lp/bugs/interfaces/bug.py	2011-09-15 19:33:27 +0000
@@ -101,7 +101,7 @@
                  status=None, datecreated=None, security_related=False,
                  private=False, subscribers=(),
                  tags=None, subscribe_owner=True, filed_by=None,
-                 importance=None, milestone=None, assignee=None):
+                 importance=None, milestone=None, assignee=None, cve=None):
         self.owner = owner
         self.title = title
         self.comment = comment
@@ -121,6 +121,7 @@
         self.importance = importance
         self.milestone = milestone
         self.assignee = assignee
+        self.cve = cve
 
     def setBugTarget(self, product=None, distribution=None,
                      sourcepackagename=None):
@@ -1143,10 +1144,13 @@
         the given bug tracker and remote bug id.
         """
 
-    def createBug(bug_params):
+    def createBug(bug_params, notify_event=True):
         """Create a bug and return it.
 
-        :bug_params: A CreateBugParams object.
+        :param bug_params: A CreateBugParams object.
+        :param notify_event: notify subscribers of the bug creation event.
+        :return: the new bug, or a tuple of bug, event when notify_event
+            is false.
 
         Things to note when using this factory:
 

=== modified file 'lib/lp/bugs/mail/commands.py'
--- lib/lp/bugs/mail/commands.py	2011-09-11 12:50:33 +0000
+++ lib/lp/bugs/mail/commands.py	2011-09-15 19:33:27 +0000
@@ -57,6 +57,7 @@
 from lp.registry.interfaces.productseries import IProductSeries
 from lp.registry.interfaces.projectgroup import IProjectGroup
 from lp.registry.interfaces.sourcepackage import ISourcePackage
+from lp.registry.interfaces.sourcepackagename import ISourcePackageName
 from lp.services.mail.commands import (
     EditEmailCommand,
     EmailCommand,
@@ -127,7 +128,7 @@
             params = CreateBugParams(
                 msg=message, title=message.title,
                 owner=getUtility(ILaunchBag).user)
-            return getUtility(IBugSet).createBugWithoutTarget(params)
+            return params, None
         else:
             try:
                 bugid = int(bugid)
@@ -181,6 +182,13 @@
                     error_templates=error_templates),
                 stop_processing=True)
 
+        if isinstance(context, CreateBugParams):
+            if context.security_related:
+                # BugSet.createBug() requires new security bugs to be private.
+                private = True
+            context.private = private
+            return context, current_event
+
         # Snapshot.
         edited_fields = set()
         if IObjectModifiedEvent.providedBy(current_event):
@@ -230,6 +238,13 @@
                     error_templates=error_templates),
                 stop_processing=True)
 
+        if isinstance(context, CreateBugParams):
+            context.security_related = security_related
+            if security_related:
+                # BugSet.createBug() requires new security bugs to be private.
+                context.private = True
+            return context, current_event
+
         # Take a snapshot.
         edited = False
         edited_fields = set()
@@ -287,6 +302,13 @@
                     'subscribe-too-many-arguments.txt',
                     error_templates=error_templates))
 
+        if isinstance(bug, CreateBugParams):
+            if len(bug.subscribers) == 0:
+                bug.subscribers = [person]
+            else:
+                bug.subscribers.append(person)
+            return bug, current_event
+
         if bug.isSubscribed(person):
             # but we still need to find the subscription
             for bugsubscription in bug.subscriptions:
@@ -308,6 +330,11 @@
 
     def execute(self, bug, current_event):
         """See IEmailCommand."""
+        if isinstance(bug, CreateBugParams):
+            # Return the input because there is not yet a bug to
+            # unsubscribe too.
+            return bug, current_event
+
         string_args = list(self.string_args)
         if len(string_args) == 1:
             person = get_person_or_team(string_args.pop())
@@ -359,6 +386,10 @@
                     'summary-too-many-arguments.txt',
                     error_templates=error_templates))
 
+        if isinstance(bug, CreateBugParams):
+            bug.title = self.string_args[0]
+            return bug, current_event
+
         return EditEmailCommand.execute(self, bug, current_event)
 
     def convertArguments(self, context):
@@ -375,6 +406,10 @@
 
     def execute(self, context, current_event):
         """See IEmailCommand."""
+        if isinstance(context, CreateBugParams):
+            # No one intentially reports a duplicate bug. Bug email commands
+            # support CreateBugParams, so in this case, just return.
+            return context, current_event
         self._ensureNumberOfArguments()
         [bug_id] = self.string_args
 
@@ -421,6 +456,10 @@
         if cve is None:
             raise EmailProcessingError(
                 'Launchpad can\'t find the CVE "%s".' % cve_sequence)
+        if isinstance(bug, CreateBugParams):
+            bug.cve = cve
+            return bug, current_event
+
         bug.linkCVE(cve, getUtility(ILaunchBag).user)
         return bug, current_event
 
@@ -528,7 +567,7 @@
         assert rest, "This is the fallback for unexpected path components."
         raise BugTargetNotFound("Unexpected path components: %s" % rest)
 
-    def execute(self, bug):
+    def execute(self, bug, bug_event):
         """See IEmailCommand."""
         if bug is None:
             raise EmailProcessingError(
@@ -551,6 +590,20 @@
         except BugTargetNotFound, error:
             raise EmailProcessingError(unicode(error), stop_processing=True)
         event = None
+
+        if isinstance(bug, CreateBugParams):
+            # Enough information has been gathered to create a new bug.
+            kwargs = {
+                'product': IProduct(bug_target, None),
+                'distribution': IDistribution(bug_target, None),
+                'sourcepackagename': ISourcePackageName(bug_target, None),
+                }
+            bug.setBugTarget(**kwargs)
+            bug, bug_event = getUtility(IBugSet).createBug(
+                bug, notify_event=False)
+            event = ObjectCreatedEvent(bug.bugtasks[0])
+            # Continue because the bug_target may be a subordinate bugtask.
+
         bugtask = bug.getBugTask(bug_target)
         if (bugtask is None and
             IDistributionSourcePackage.providedBy(bug_target)):
@@ -568,7 +621,7 @@
             bugtask = self._create_bug_task(bug, bug_target)
             event = ObjectCreatedEvent(bugtask)
 
-        return bugtask, event
+        return bugtask, event, bug_event
 
     def _targetBug(self, user, bug, series, sourcepackagename=None):
         """Try to target the bug the given distroseries.
@@ -796,16 +849,10 @@
         """See `IEmailCommand`."""
         # Tags are always lowercase.
         string_args = [arg.lower() for arg in self.string_args]
-        # Bug.tags returns a Zope List, which does not support Python list
-        # operations so we need to convert it.
-        tags = list(bug.tags)
-
-        # XXX: DaveMurphy 2007-07-11: in the following loop we process each
-        # tag in turn. Each tag that is either invalid or unassigned will
-        # result in a mail to the submitter. This may result in several mails
-        # for a single command. This will need to be addressed if that becomes
-        # a problem.
-
+        if bug.tags is None:
+            tags = []
+        else:
+            tags = list(bug.tags)
         for arg in string_args:
             # Are we adding or removing a tag?
             if arg.startswith('-'):
@@ -832,13 +879,6 @@
                             tag=tag))
             else:
                 tags.append(arg)
-
-        # Duplicates are dealt with when the tags are stored in the DB (which
-        # incidentally uses a set to achieve this). Since the code already
-        # exists we don't duplicate it here.
-
-        # Bug.tags expects to be given a Python list, so there is no need to
-        # convert it back.
         bug.tags = tags
 
         return bug, current_event

=== modified file 'lib/lp/bugs/mail/handler.py'
--- lib/lp/bugs/mail/handler.py	2011-08-29 20:04:54 +0000
+++ lib/lp/bugs/mail/handler.py	2011-09-15 19:33:27 +0000
@@ -30,6 +30,7 @@
     IBugAttachmentSet,
     )
 from lp.bugs.interfaces.bugmessage import IBugMessageSet
+from lp.bugs.interfaces.bug import CreateBugParams
 from lp.bugs.mail.commands import BugEmailCommands
 from lp.services.mail.helpers import (
     ensure_not_weakly_authenticated,
@@ -273,18 +274,20 @@
                             message = self.appendBugComment(
                                 bug, signed_msg, filealias)
                             add_comment_to_bug = False
-                        else:
+                            self.processAttachments(bug, message, signed_msg)
+                    elif IBugTaskEmailCommand.providedBy(command):
+                        self.notify_bugtask_event(bugtask_event, bug_event)
+                        bugtask, bugtask_event, bug_event = command.execute(
+                            bug, bug_event)
+                        if isinstance(bug, CreateBugParams):
+                            bug = bugtask.bug
                             message = bug.initial_message
-                        self.processAttachments(bug, message, signed_msg)
-                    elif IBugTaskEmailCommand.providedBy(command):
-                        self.notify_bugtask_event(bugtask_event, bug_event)
-                        bugtask, bugtask_event = command.execute(
-                            bug)
+                            self.processAttachments(bug, message, signed_msg)
                     elif IBugEditEmailCommand.providedBy(command):
                         bug, bug_event = command.execute(bug, bug_event)
                     elif IBugTaskEditEmailCommand.providedBy(command):
                         if bugtask is None:
-                            if len(bug.bugtasks) == 0:
+                            if isinstance(bug, CreateBugParams):
                                 self.handleNoAffectsTarget()
                             bugtask = guess_bugtask(
                                 bug, getUtility(ILaunchBag).user)
@@ -306,6 +309,9 @@
                     '\n'.join(str(error) for error, command
                               in processing_errors),
                     [command for error, command in processing_errors])
+            if isinstance(bug, CreateBugParams):
+                # A new bug without any commands was sent.
+                self.handleNoAffectsTarget()
             self.notify_bug_event(bug_event)
             self.notify_bugtask_event(bugtask_event, bug_event)
 

=== modified file 'lib/lp/bugs/mail/tests/test_commands.py'
--- lib/lp/bugs/mail/tests/test_commands.py	2011-08-16 18:39:51 +0000
+++ lib/lp/bugs/mail/tests/test_commands.py	2011-09-15 19:33:27 +0000
@@ -1,11 +1,32 @@
 # Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-from canonical.testing.layers import DatabaseFunctionalLayer
+from lazr.lifecycle.interfaces import (
+    IObjectCreatedEvent,
+    IObjectModifiedEvent,
+    )
+
+from canonical.testing.layers import (
+    DatabaseFunctionalLayer,
+    LaunchpadFunctionalLayer,
+    )
+from lp.bugs.interfaces.bug import CreateBugParams
 from lp.bugs.mail.commands import (
     AffectsEmailCommand,
-    )
-from lp.services.mail.interfaces import BugTargetNotFound
+    BugEmailCommand,
+    CVEEmailCommand,
+    DuplicateEmailCommand,
+    PrivateEmailCommand,
+    SecurityEmailCommand,
+    SubscribeEmailCommand,
+    SummaryEmailCommand,
+    TagEmailCommand,
+    UnsubscribeEmailCommand,
+    )
+from lp.services.mail.interfaces import (
+    BugTargetNotFound,
+    EmailProcessingError,
+    )
 from lp.testing import (
     login_celebrity,
     login_person,
@@ -143,3 +164,429 @@
         self.assertRaisesWithContent(
             BugTargetNotFound, message,
             AffectsEmailCommand.getBugTarget, 'fnord/pting/snarf/thrup')
+
+    def test_execute_bug(self):
+        bug = self.factory.makeBug()
+        product = self.factory.makeProduct(name='fnord')
+        login_person(bug.bugtasks[0].target.owner)
+        command = AffectsEmailCommand('affects', ['fnord'])
+        bugtask, bugtask_event, bug_event = command.execute(bug, None)
+        self.assertEqual(bug, bugtask.bug)
+        self.assertEqual(product, bugtask.target)
+        self.assertTrue(IObjectCreatedEvent.providedBy(bugtask_event))
+        self.assertEqual(None, bug_event)
+
+    def test_execute_bug_params_product(self):
+        user = self.factory.makePerson()
+        login_person(user)
+        product = self.factory.makeProduct(name='fnord')
+        message = self.factory.makeMessage(
+            subject='bug title', content='borked\n affects fnord')
+        command = AffectsEmailCommand('affects', ['fnord'])
+        bug_params = CreateBugParams(
+            title='bug title', msg=message, owner=user)
+        bugtask, bugtask_event, bug_event = command.execute(bug_params, None)
+        self.assertEqual(product, bugtask.target)
+        self.assertEqual('bug title', bugtask.bug.title)
+        self.assertEqual('borked\n affects fnord', bugtask.bug.description)
+        self.assertEqual(user, bugtask.bug.owner)
+        self.assertTrue(IObjectCreatedEvent.providedBy(bugtask_event))
+        self.assertTrue(IObjectCreatedEvent.providedBy(bug_event))
+
+    def test_execute_bug_params_productseries(self):
+        product = self.factory.makeProduct(name='fnord')
+        login_person(product.owner)
+        series = self.factory.makeProductSeries(name='pting', product=product)
+        message = self.factory.makeMessage(
+            subject='bug title', content='borked\n affects fnord/pting')
+        command = AffectsEmailCommand('affects', ['fnord/pting'])
+        bug_params = CreateBugParams(
+            title='bug title', msg=message, owner=product.owner)
+        bugtask, bugtask_event, bug_event = command.execute(bug_params, None)
+        self.assertEqual(series, bugtask.target)
+        self.assertEqual('bug title', bugtask.bug.title)
+        self.assertEqual(2, len(bugtask.bug.bugtasks))
+        self.assertTrue(IObjectCreatedEvent.providedBy(bugtask_event))
+        self.assertTrue(IObjectCreatedEvent.providedBy(bug_event))
+
+    def test_execute_bug_params_distribution(self):
+        user = self.factory.makePerson()
+        login_person(user)
+        distribution = self.factory.makeDistribution(name='fnord')
+        message = self.factory.makeMessage(
+            subject='bug title', content='borked\n affects fnord')
+        command = AffectsEmailCommand('affects', ['fnord'])
+        bug_params = CreateBugParams(
+            title='bug title', msg=message, owner=user)
+        bugtask, bugtask_event, bug_event = command.execute(bug_params, None)
+        self.assertEqual(distribution, bugtask.target)
+        self.assertEqual('bug title', bugtask.bug.title)
+        self.assertTrue(IObjectCreatedEvent.providedBy(bugtask_event))
+        self.assertTrue(IObjectCreatedEvent.providedBy(bug_event))
+
+    def test_execute_bug_params_dsp(self):
+        user = self.factory.makePerson()
+        login_person(user)
+        distribution = self.factory.makeDistribution(name='fnord')
+        series = self.factory.makeDistroSeries(
+            name='pting', distribution=distribution)
+        package = self.factory.makeSourcePackage(
+            sourcepackagename='snarf', distroseries=series, publish=True)
+        dsp = distribution.getSourcePackage(package.name)
+        message = self.factory.makeMessage(
+            subject='bug title', content='borked\n affects fnord/snarf')
+        command = AffectsEmailCommand('affects', ['fnord/snarf'])
+        bug_params = CreateBugParams(
+            title='bug title', msg=message, owner=user)
+        bugtask, bugtask_event, bug_event = command.execute(bug_params, None)
+        self.assertEqual(dsp, bugtask.target)
+        self.assertEqual('bug title', bugtask.bug.title)
+        self.assertTrue(IObjectCreatedEvent.providedBy(bugtask_event))
+        self.assertTrue(IObjectCreatedEvent.providedBy(bug_event))
+
+    def test_execute_bug_params_distroseries(self):
+        distribution = self.factory.makeDistribution(name='fnord')
+        login_person(distribution.owner)
+        series = self.factory.makeDistroSeries(
+            name='pting', distribution=distribution)
+        message = self.factory.makeMessage(
+            subject='bug title', content='borked\n affects fnord/pting')
+        command = AffectsEmailCommand('affects', ['fnord/pting'])
+        bug_params = CreateBugParams(
+            title='bug title', msg=message, owner=distribution.owner)
+        bugtask, bugtask_event, bug_event = command.execute(bug_params, None)
+        self.assertEqual(series, bugtask.target)
+        self.assertEqual('bug title', bugtask.bug.title)
+        self.assertEqual(2, len(bugtask.bug.bugtasks))
+        self.assertTrue(IObjectCreatedEvent.providedBy(bugtask_event))
+        self.assertTrue(IObjectCreatedEvent.providedBy(bug_event))
+
+    def test_execute_bug_params_distroseries_sourcepackage(self):
+        distribution = self.factory.makeDistribution(name='fnord')
+        login_person(distribution.owner)
+        series = self.factory.makeDistroSeries(
+            name='pting', distribution=distribution)
+        package = self.factory.makeSourcePackage(
+            sourcepackagename='snarf', distroseries=series, publish=True)
+        message = self.factory.makeMessage(
+            subject='bug title', content='borked\n affects fnord/pting/snarf')
+        command = AffectsEmailCommand('affects', ['fnord/pting/snarf'])
+        bug_params = CreateBugParams(
+            title='bug title', msg=message, owner=distribution.owner)
+        bugtask, bugtask_event, bug_event = command.execute(bug_params, None)
+        self.assertEqual(package, bugtask.target)
+        self.assertEqual('bug title', bugtask.bug.title)
+        self.assertEqual(2, len(bugtask.bug.bugtasks))
+        self.assertTrue(IObjectCreatedEvent.providedBy(bugtask_event))
+        self.assertTrue(IObjectCreatedEvent.providedBy(bug_event))
+
+
+class BugEmailCommandTestCase(TestCaseWithFactory):
+
+    layer = LaunchpadFunctionalLayer
+
+    def test_execute_bug_id(self):
+        bug = self.factory.makeBug()
+        command = BugEmailCommand('bug', [str(bug.id)])
+        self.assertEqual((bug, None), command.execute(None, None))
+
+    def test_execute_bug_id_wrong_type(self):
+        command = BugEmailCommand('bug', ['nickname'])
+        error = self.assertRaises(
+            EmailProcessingError, command.execute, None, None)
+        message = str(error).split('\n')
+        self.assertEqual(
+            "The 'bug' command expects either 'new' or a bug id.", message[0])
+
+    def test_execute_bug_id_not_found(self):
+        command = BugEmailCommand('bug', ['9999999'])
+        error = self.assertRaises(
+            EmailProcessingError, command.execute, None, None)
+        message = str(error).split('\n')
+        self.assertEqual(
+            "There is no such bug in Launchpad: 9999999", message[0])
+
+    def test_execute_bug_id_new(self):
+        user = self.factory.makePerson()
+        login_person(user)
+        message = self.factory.makeSignedMessage(
+            body='borked\n affects fnord',
+            subject='title borked',
+            to_address='new@xxxxxxxxxxxxxxxxxx')
+        filealias = self.factory.makeLibraryFileAlias()
+        command = BugEmailCommand('bug', ['new'])
+        params, event = command.execute(message, filealias)
+        self.assertEqual(None, event)
+        self.assertEqual(user, params.owner)
+        self.assertEqual('title borked', params.title)
+        self.assertEqual(message['Message-Id'], params.msg.rfc822msgid)
+
+
+class PrivateEmailCommandTestCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_execute_bug(self):
+        bug = self.factory.makeBug()
+        login_person(bug.bugtasks[0].target.owner)
+        command = PrivateEmailCommand('private', ['yes'])
+        exec_bug, event = command.execute(bug, None)
+        self.assertEqual(bug, exec_bug)
+        self.assertEqual(True, bug.private)
+        self.assertTrue(IObjectModifiedEvent.providedBy(event))
+
+    def test_execute_bug_params(self):
+        user = self.factory.makePerson()
+        login_person(user)
+        bug_params = CreateBugParams(title='bug title', owner=user)
+        command = PrivateEmailCommand('private', ['yes'])
+        dummy_event = object()
+        params, event = command.execute(bug_params, dummy_event)
+        self.assertEqual(bug_params, params)
+        self.assertEqual(True, bug_params.private)
+        self.assertEqual(dummy_event, event)
+
+    def test_execute_bug_params_with_security(self):
+        # BugSet.createBug() requires new security bugs to be private.
+        user = self.factory.makePerson()
+        login_person(user)
+        bug_params = CreateBugParams(
+            title='bug title', owner=user, security_related='yes')
+        command = PrivateEmailCommand('private', ['no'])
+        dummy_event = object()
+        params, event = command.execute(bug_params, dummy_event)
+        self.assertEqual(bug_params, params)
+        self.assertEqual(True, bug_params.private)
+        self.assertEqual(dummy_event, event)
+
+
+class SecurityEmailCommandTestCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_execute_bug(self):
+        bug = self.factory.makeBug()
+        login_person(bug.bugtasks[0].target.owner)
+        command = SecurityEmailCommand('security', ['yes'])
+        exec_bug, event = command.execute(bug, None)
+        self.assertEqual(bug, exec_bug)
+        self.assertEqual(True, bug.security_related)
+        self.assertTrue(IObjectModifiedEvent.providedBy(event))
+
+    def test_execute_bug_params(self):
+        user = self.factory.makePerson()
+        login_person(user)
+        bug_params = CreateBugParams(title='bug title', owner=user)
+        command = SecurityEmailCommand('security', ['yes'])
+        dummy_event = object()
+        params, event = command.execute(bug_params, dummy_event)
+        self.assertEqual(bug_params, params)
+        self.assertEqual(True, bug_params.security_related)
+        self.assertEqual(True, bug_params.private)
+        self.assertEqual(dummy_event, event)
+
+
+class SubscribeEmailCommandTestCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_execute_bug_with_user_name(self):
+        bug = self.factory.makeBug()
+        login_person(bug.bugtasks[0].target.owner)
+        subscriber = self.factory.makePerson()
+        command = SubscribeEmailCommand('subscribe', [subscriber.name])
+        dummy_event = object()
+        exec_bug, event = command.execute(bug, dummy_event)
+        self.assertEqual(bug, exec_bug)
+        self.assertContentEqual(
+            [bug.owner, subscriber], bug.getDirectSubscribers())
+        self.assertEqual(dummy_event, event)
+
+    def test_execute_bug_without_user_name(self):
+        bug = self.factory.makeBug()
+        target_owner = bug.bugtasks[0].target.owner
+        login_person(target_owner)
+        command = SubscribeEmailCommand('subscribe', [])
+        dummy_event = object()
+        exec_bug, event = command.execute(bug, dummy_event)
+        self.assertEqual(bug, exec_bug)
+        self.assertContentEqual(
+            [bug.owner, target_owner], bug.getDirectSubscribers())
+        self.assertEqual(dummy_event, event)
+
+    def test_execute_bug_params_one_subscriber(self):
+        user = self.factory.makePerson()
+        login_person(user)
+        subscriber = self.factory.makePerson()
+        bug_params = CreateBugParams(title='bug title', owner=user)
+        command = SubscribeEmailCommand('subscribe', [subscriber.name])
+        dummy_event = object()
+        params, event = command.execute(bug_params, dummy_event)
+        self.assertEqual(bug_params, params)
+        self.assertContentEqual([subscriber], bug_params.subscribers)
+        self.assertEqual(dummy_event, event)
+
+    def test_execute_bug_params_many_subscriber(self):
+        user = self.factory.makePerson()
+        login_person(user)
+        subscriber_1 = self.factory.makePerson()
+        subscriber_2 = self.factory.makePerson()
+        bug_params = CreateBugParams(
+            title='bug title', owner=user, subscribers=[subscriber_1])
+        command = SubscribeEmailCommand('subscribe', [subscriber_2.name])
+        dummy_event = object()
+        params, event = command.execute(bug_params, dummy_event)
+        self.assertEqual(bug_params, params)
+        self.assertContentEqual(
+            [subscriber_1, subscriber_2], bug_params.subscribers)
+        self.assertEqual(dummy_event, event)
+
+
+class UnsubscribeEmailCommandTestCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_execute_bug_with_user_name(self):
+        bug = self.factory.makeBug()
+        target_owner = bug.bugtasks[0].target.owner
+        login_person(target_owner)
+        bug.subscribe(target_owner, target_owner)
+        command = UnsubscribeEmailCommand('unsubscribe', [target_owner.name])
+        dummy_event = object()
+        exec_bug, event = command.execute(bug, dummy_event)
+        self.assertEqual(bug, exec_bug)
+        self.assertContentEqual(
+            [bug.owner], bug.getDirectSubscribers())
+        self.assertEqual(dummy_event, event)
+
+    def test_execute_bug_without_user_name(self):
+        bug = self.factory.makeBug()
+        target_owner = bug.bugtasks[0].target.owner
+        login_person(target_owner)
+        bug.subscribe(target_owner, target_owner)
+        command = UnsubscribeEmailCommand('unsubscribe', [])
+        dummy_event = object()
+        exec_bug, event = command.execute(bug, dummy_event)
+        self.assertEqual(bug, exec_bug)
+        self.assertContentEqual(
+            [bug.owner], bug.getDirectSubscribers())
+        self.assertEqual(dummy_event, event)
+
+    def test_execute_bug_params(self):
+        # Unsubscribe does nothing because the is not yet a bug.
+        # Any value can be used for the user name.
+        user = self.factory.makePerson()
+        login_person(user)
+        bug_params = CreateBugParams(title='bug title', owner=user)
+        command = UnsubscribeEmailCommand('unsubscribe', ['non-existent'])
+        dummy_event = object()
+        params, event = command.execute(bug_params, dummy_event)
+        self.assertEqual(bug_params, params)
+        self.assertEqual(dummy_event, event)
+
+
+class SummaryEmailCommandTestCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_execute_bug(self):
+        bug = self.factory.makeBug()
+        login_person(bug.bugtasks[0].target.owner)
+        command = SummaryEmailCommand('summary', ['new title'])
+        exec_bug, event = command.execute(bug, None)
+        self.assertEqual(bug, exec_bug)
+        self.assertEqual('new title', bug.title)
+        self.assertTrue(IObjectModifiedEvent.providedBy(event))
+
+    def test_execute_bug_params(self):
+        user = self.factory.makePerson()
+        login_person(user)
+        bug_params = CreateBugParams(title='bug title', owner=user)
+        command = SummaryEmailCommand('summary', ['new title'])
+        dummy_event = object()
+        params, event = command.execute(bug_params, dummy_event)
+        self.assertEqual(bug_params, params)
+        self.assertEqual('new title', bug_params.title)
+        self.assertEqual(dummy_event, event)
+
+
+class DuplicateEmailCommandTestCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_execute_bug(self):
+        master_bug = self.factory.makeBug()
+        bug = self.factory.makeBug()
+        login_person(bug.bugtasks[0].target.owner)
+        command = DuplicateEmailCommand('duplicate', [str(master_bug.id)])
+        exec_bug, event = command.execute(bug, None)
+        self.assertEqual(master_bug, exec_bug)
+        self.assertEqual(master_bug, bug.duplicateof)
+        self.assertTrue(IObjectModifiedEvent.providedBy(event))
+
+    def test_execute_bug_params(self):
+        # duplicate does nothing because the is not yet a bug.
+        # Any value can be used for the bug is.
+        user = self.factory.makePerson()
+        login_person(user)
+        bug_params = CreateBugParams(title='bug title', owner=user)
+        command = DuplicateEmailCommand('duplicate', ['non-existent'])
+        dummy_event = object()
+        params, event = command.execute(bug_params, dummy_event)
+        self.assertEqual(bug_params, params)
+        self.assertEqual(dummy_event, event)
+
+
+class CVEEmailCommandTestCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_execute_bug(self):
+        bug = self.factory.makeBug()
+        login_person(bug.bugtasks[0].target.owner)
+        cve = self.factory.makeCVE('1999-1717')
+        command = CVEEmailCommand('cve', ['1999-1717'])
+        dummy_event = object()
+        exec_bug, event = command.execute(bug, dummy_event)
+        self.assertEqual(bug, exec_bug)
+        self.assertEqual([cve], [cve_link.cve for cve_link in bug.cve_links])
+        self.assertEqual(dummy_event, event)
+
+    def test_execute_bug_params(self):
+        user = self.factory.makePerson()
+        login_person(user)
+        cve = self.factory.makeCVE('1999-1717')
+        bug_params = CreateBugParams(title='bug title', owner=user)
+        command = CVEEmailCommand('cve', ['1999-1717'])
+        dummy_event = object()
+        params, event = command.execute(bug_params, dummy_event)
+        self.assertEqual(bug_params, params)
+        self.assertEqual(cve, params.cve)
+        self.assertEqual(dummy_event, event)
+
+
+class TagEmailCommandTestCase(TestCaseWithFactory):
+
+    layer = DatabaseFunctionalLayer
+
+    def test_execute_bug(self):
+        bug = self.factory.makeBug()
+        login_person(bug.bugtasks[0].target.owner)
+        bug.tags = ['form']
+        command = TagEmailCommand('tag', ['ui', 'trivial'])
+        dummy_event = object()
+        exec_bug, event = command.execute(bug, dummy_event)
+        self.assertEqual(bug, exec_bug)
+        self.assertContentEqual(['form', 'ui', 'trivial'], bug.tags)
+        self.assertEqual(dummy_event, event)
+
+    def test_execute_bug_params(self):
+        user = self.factory.makePerson()
+        login_person(user)
+        bug_params = CreateBugParams(title='bug title', owner=user)
+        command = TagEmailCommand('tag', ['ui', 'trivial'])
+        dummy_event = object()
+        params, event = command.execute(bug_params, dummy_event)
+        self.assertEqual(bug_params, params)
+        self.assertContentEqual(['ui', 'trivial'], bug_params.tags)
+        self.assertEqual(dummy_event, event)

=== modified file 'lib/lp/bugs/model/bug.py'
--- lib/lp/bugs/model/bug.py	2011-09-13 22:43:21 +0000
+++ lib/lp/bugs/model/bug.py	2011-09-15 19:33:27 +0000
@@ -228,7 +228,7 @@
             "distribution", "sourcepackagename",
             "product", "status", "subscribers", "tags",
             "subscribe_owner", "filed_by", "importance",
-            "milestone", "assignee"])
+            "milestone", "assignee", "cve"])
 
 
 class BugTag(SQLBase):
@@ -2606,7 +2606,7 @@
                 orderBy=['datecreated'])
         return bug
 
-    def createBug(self, bug_params):
+    def createBug(self, bug_params, notify_event=True):
         """See `IBugSet`."""
         # Make a copy of the parameter object, because we might modify some
         # of its attribute values below.
@@ -2668,11 +2668,14 @@
             bug_task.transitionToMilestone(params.milestone, params.owner)
 
         # Tell everyone.
-        notify(event)
+        if notify_event:
+            notify(event)
 
         # Calculate the bug's initial heat.
         bug.updateHeat()
 
+        if not notify_event:
+            return bug, event
         return bug
 
     def createBugWithoutTarget(self, bug_params):
@@ -2736,6 +2739,9 @@
         # Mark the bug reporter as affected by that bug.
         bug.markUserAffected(bug.owner)
 
+        if params.cve is not None:
+            bug.linkCVE(params.cve, params.owner)
+
         # Populate the creation event.
         if params.filed_by is None:
             event = ObjectCreatedEvent(bug, user=params.owner)

=== modified file 'lib/lp/bugs/tests/test_bug.py'
--- lib/lp/bugs/tests/test_bug.py	2011-05-24 08:58:38 +0000
+++ lib/lp/bugs/tests/test_bug.py	2011-09-15 19:33:27 +0000
@@ -287,3 +287,14 @@
             self.assertRaises(
                 UserCannotEditBugTaskMilestone,
                 getUtility(IBugSet).createBug, params)
+
+    def test_createBugWithoutTarget_cve(self):
+        cve = self.factory.makeCVE('1999-1717')
+        target = self.factory.makeProduct()
+        person = self.factory.makePerson()
+        with person_logged_in(person):
+            params = CreateBugParams(
+                owner=person, title="A bug", comment="bad thing.", cve=cve)
+        params.setBugTarget(product=target)
+        bug = getUtility(IBugSet).createBug(params)
+        self.assertEqual([cve], [cve_link.cve for cve_link in bug.cve_links])

=== modified file 'lib/lp/registry/adapters.py'
--- lib/lp/registry/adapters.py	2011-08-19 15:29:12 +0000
+++ lib/lp/registry/adapters.py	2011-09-15 19:33:27 +0000
@@ -130,3 +130,8 @@
     # Used for traversal from distro to +pubconf.
     config = getUtility(IPublisherConfigSet).getByDistribution(distro)
     return config
+
+
+def package_to_sourcepackagename(package):
+    """Adapts a package to its `ISourcePackageName`."""
+    return package.sourcepackagename

=== modified file 'lib/lp/registry/configure.zcml'
--- lib/lp/registry/configure.zcml	2011-09-10 00:16:56 +0000
+++ lib/lp/registry/configure.zcml	2011-09-15 19:33:27 +0000
@@ -554,6 +554,11 @@
         factory="lp.registry.adapters.sourcepackage_to_distribution"
         permission="zope.Public"/>
     <adapter
+        provides="lp.registry.interfaces.sourcepackagename.ISourcePackageName"
+        for="lp.registry.interfaces.distributionsourcepackage.IDistributionSourcePackage"
+        factory="lp.registry.adapters.package_to_sourcepackagename"
+        permission="zope.Public"/>
+    <adapter
         factory="lp.registry.browser.distributionsourcepackage.DistributionSourcePackageBreadcrumb"/>
 
     <!-- CommercialSubscription -->
@@ -1478,6 +1483,11 @@
         factory="lp.registry.adapters.productseries_to_product"
         permission="zope.Public"/>
     <adapter
+        provides="lp.registry.interfaces.product.IProduct"
+        for="lp.registry.interfaces.productseries.IProductSeries"
+        factory="lp.registry.adapters.productseries_to_product"
+        permission="zope.Public"/>
+    <adapter
         provides="canonical.launchpad.webapp.interfaces.IBreadcrumb"
         for="lp.registry.interfaces.productseries.IProductSeries"
         factory="lp.registry.browser.productseries.ProductSeriesBreadcrumb"
@@ -1670,6 +1680,11 @@
     <adapter
         factory="lp.registry.browser.sourcepackage.SourcePackageBreadcrumb"/>
     <adapter
+        provides="lp.registry.interfaces.sourcepackagename.ISourcePackageName"
+        for="lp.registry.interfaces.sourcepackage.ISourcePackage"
+        factory="lp.registry.adapters.package_to_sourcepackagename"
+        permission="zope.Public"/>
+    <adapter
         provides="lp.app.interfaces.launchpad.IServiceUsage"
         for="lp.registry.interfaces.sourcepackage.ISourcePackage"
         factory="lp.registry.adapters.sourcepackage_to_distribution"

=== modified file 'lib/lp/registry/tests/test_adapters.py'
--- lib/lp/registry/tests/test_adapters.py	2011-07-25 19:21:49 +0000
+++ lib/lp/registry/tests/test_adapters.py	2011-09-15 19:33:27 +0000
@@ -8,11 +8,13 @@
 from canonical.testing.layers import DatabaseFunctionalLayer
 from lp.registry.adapters import (
     distroseries_to_distribution,
+    package_to_sourcepackagename,
     productseries_to_product,
     sourcepackage_to_distribution,
     )
 from lp.registry.interfaces.distribution import IDistribution
 from lp.registry.interfaces.product import IProduct
+from lp.registry.interfaces.sourcepackagename import ISourcePackageName
 from lp.testing import TestCaseWithFactory
 
 
@@ -37,6 +39,22 @@
         self.assertEqual(
             package.distroseries.distribution, IDistribution(package))
 
+    def test_sourcepackage_to_sourcepackagename(self):
+        # A sourcepackagename can be retrieved source package.
+        package = self.factory.makeSourcePackage()
+        spn = package_to_sourcepackagename(package)
+        self.assertTrue(ISourcePackageName.providedBy(spn))
+        self.assertEqual(
+            package.sourcepackagename, ISourcePackageName(package))
+
+    def test_distributionsourcepackage_to_sourcepackagename(self):
+        # A sourcepackagename can be retrieved distribution source package.
+        package = self.factory.makeDistributionSourcePackage()
+        spn = package_to_sourcepackagename(package)
+        self.assertTrue(ISourcePackageName.providedBy(spn))
+        self.assertEqual(
+            package.sourcepackagename, ISourcePackageName(package))
+
     def test_distroseries_to_distribution(self):
         # distroseries_to_distribution() returns an IDistribution given an
         # IDistroSeries.
@@ -54,3 +72,4 @@
         product = productseries_to_product(product_series)
         self.assertTrue(IProduct.providedBy(product))
         self.assertEqual(product_series.product, product)
+        self.assertEqual(product, IProduct(product_series))