Repository: fedora-infra/bodhi

Issue:
bodhi resume push spews full updates details rather than just a list of NVRs
So when you use to resume a bodhi push it use to just provide a list of NVR updates just like when doing a new push. Now you get a list of the full output. Sample included below. It's too verbose and doesn't really provide useful output you can browse over to see if it looks sane.

<pre>
================================================================================
     cinnamon-translations-3.2.0-1.fc24 cinnamon-3.2.1-1.fc24 nemo-
     extensions-3.2.0-1.fc24 nemo-3.2.0-1.fc24 cinnamon-control-
     center-3.2.0-1.fc24 muffin-3.2.0-1.fc24 cinnamon-session-3.2.0-1.fc24
     cinnamon-settings-daemon-3.2.0-1.fc24 cinnamon-screensaver-3.2.0-2.fc24
     cinnamon-menus-3.2.0-1.fc24 cinnamon-desktop-3.2.0-3.fc24
     xapps-1.0.2-1.fc24 cjs-3.2.0-1.fc24
================================================================================
  Update ID: FEDORA-2016-a3e0d84cac
    Release: Fedora 24
     Status: pending
       Type: enhancement
      Karma: 0
    Request: testing
      Notes: Cinnamon-3.2.x update  - Input driver switched from synaptics to
           : libinput.
  Submitter: leigh123linux
  Submitted: 2016-11-15 15:39:45.321725
   Comments: bodhi - 2016-11-15 15:39:45.445701 (karma 0)
             This update has been submitted for testing by
             leigh123linux.

  https://bodhi.fedoraproject.org/updates/FEDORA-2016-a3e0d84cac

================================================================================
     openvas-scanner-5.0.7-1.fc24
================================================================================
  Update ID: FEDORA-2016-1212611d92
    Release: Fedora 24
     Status: pending
       Type: enhancement
      Karma: 0
    Request: testing
      Notes: Bugfix release 5.0.7
  Submitter: rebus
  Submitted: 2016-11-15 15:49:31.789280
   Comments: bodhi - 2016-11-15 15:49:31.821316 (karma 0)
             This update has been submitted for testing by rebus.

  https://bodhi.fedoraproject.org/updates/FEDORA-2016-1212611d92

================================================================================
     erlang-lager-3.2.4-1.fc24
================================================================================
  Update ID: FEDORA-2016-893e84257f
    Release: Fedora 24
     Status: pending
       Type: enhancement
      Karma: 0
    Request: testing
       Bugs: 1378633 - erlang-lager-3.2.4 is available
      Notes: * Ver. 3.2.4
  Submitter: peter
  Submitted: 2016-11-15 16:00:21.990581
   Comments: bodhi - 2016-11-15 16:00:22.005409 (karma 0)
             This update has been submitted for testing by peter.

  https://bodhi.fedoraproject.org/updates/FEDORA-2016-893e84257f

================================================================================
     perl-Crypt-IDEA-1.10-5.fc24
================================================================================
  Update ID: FEDORA-2016-e85db2ba89
    Release: Fedora 24
     Status: pending
       Type: newpackage
      Karma: 0
    Request: testing
       Bugs: 1395121 - Review Request: perl-Crypt-IDEA - Perl interface to IDEA
           : block cipher
      Notes: This is the first Fedora release of perl-Crypt-IDEA. The package is
           : moving from RPM Fusion to Fedora as the IDEA patent
           : expired a few years ago.
  Submitter: pghmcfc
  Submitted: 2016-11-15 16:31:10.134744
   Comments: bodhi - 2016-11-15 16:31:10.458556 (karma 0)
             This update has been submitted for testing by pghmcfc.

  https://bodhi.fedoraproject.org/updates/FEDORA-2016-e85db2ba89


</pre>


Discussion:
Oh strangely it then seems to print the whole lot at the end. I had two to resume (24-u-t and 23):

<pre>
================================================================================
     pidgin-groupchat-typing-notifications-0-2.git33a75f9.fc23
================================================================================
  Update ID: FEDORA-2016-f0639c407b
    Release: Fedora 23
     Status: testing
       Type: newpackage
      Karma: 0
    Request: stable
       Bugs: 1392227 - Review Request: pidgin-groupchat-typing-notifications -
           : Adds typing notifications for group chats in Pidgin
      Notes: Small spec fixes.  ----  Initial upload.
  Submitter: xvitaly
  Submitted: 2016-11-07 17:02:19.374737
   Comments: bodhi - 2016-11-07 17:02:19.521718 (karma 0)
             This update has been submitted for testing by xvitaly.
             bodhi - 2016-11-07 17:02:36.204530 (karma 0)
             This update has obsoleted [pidgin-groupchat-typing-not
             ifications-0-1.git33a75f9.fc23](https://bodhi.fedorapr
             oject.org/updates/FEDORA-2016-49e258e3cc), and has
             inherited its bugs and notes.
             bodhi - 2016-11-08 01:30:39.879582 (karma 0)
             This update has been pushed to testing.
             bodhi - 2016-11-15 00:00:54.652583 (karma 0)
             This update has reached 7 days in testing and can be
             pushed to stable now if the maintainer wishes
             bodhi - 2016-11-15 00:05:15.071462 (karma 0)
             This update has been submitted for stable by xvitaly.
  https://bodhi.fedoraproject.org/updates/FEDORA-2016-f0639c407b
plee-the-bear-0.7.0-9.fc24
perl-Date-Extract-0.06-1.fc24
bluedevil-5.8.3-1.fc24 breeze-gtk-5.8.3-1.fc24 grub2-breeze-theme-5.8.3-1.fc24 kactivitymanagerd-5.8.3-1.fc24 kde-cli-tools-5.8.3-1.fc24 kde-gtk-config-5.8.3-1.fc24 kde-settings-24-8.fc24 kdecoration-5.8.3-1.fc24 kdeplasma-addons-5.8.3-1.fc24 kgamma-5.8.3-1.fc24 khotkeys-5.8.3-1.fc24 kinfocenter-5.8.3-1.fc24 kmenuedit-5.8.3-1.fc24 kscreen-5.8.3-1.fc24 kscreenlocker-5.8.3-1.fc24 ksshaskpass-5.8.3-1.fc24 ksysguard-5.8.3-1.fc24 kwayland-integration-5.8.3-1.fc24 kwin-5.8.3-1.fc24 kwrited-5.8.3-1.fc24 libkscreen-qt5-5.8.3-1.fc24 libksysguard-5.8.3-1.fc24 pam-kwallet-5.8.3-1.fc24 plasma-breeze-5.8.3-1.fc24 plasma-desktop-5.8.3-2.fc24 plasma-discover-5.8.3-1.fc24 plasma-integration-5.8.3-1.fc24 plasma-milou-5.8.3-1.fc24 plasma-nm-5.8.3-1.fc24 plasma-oxygen-5.8.3-1.fc24 plasma-pa-5.8.3-1.fc24 plasma-sdk-5.8.3-1.fc24 plasma-systemsettings-5.8.3-1.fc24 plasma-user-manager-5.8.3-1.fc24 plasma-workspace-5.8.3-1.fc24 plasma-workspace-wallpapers-5.8.3-1.fc24 plymouth-theme-breeze-5.8.3-1.fc24 polkit-kde-5.8.3-1.fc24 powerdevil-5.8.3-1.fc24 sddm-kcm-5.8.3-1.fc24
perl-Encode-IMAPUTF7-1.05-2.fc24 perl-Mail-IMAPTalk-4.04-1.fc24
moodle-3.1.3-1.fc24
znc-clientbuffer-0-0.1gitfe0f368.fc24
python-serpy-0.1.1-1.fc24
gimp-focusblur-plugin-3.2.6-1.fc24
php-simplesamlphp-saml2_1-1.10.2-1.fc24 php-simplesamlphp-saml2-2.3.2-1.fc24
kelbt-0.16-2.fc24
meson-0.36.0-1.fc24
sparse-0.5.0-8.fc24
lldb-3.8.0-3.fc24
mbuffer-20160613-1.fc24
moin-1.9.9-1.fc24
python-requestbuilder-0.6.0-1.fc24
ocaml-menhir-20161114-1.fc24
xorg-x11-server-1.18.4-5.fc24
flacon-2.1.0-1.fc24
php-phpunit-PHPUnit-5.6.3-1.fc24 php-sebastian-recursion-context-1.0.4-1.fc24
siege-4.0.2-2.fc24
libdrm-2.4.73-1.fc24
nfoview-1.21-1.fc24
perl-Data-ICal-TimeZone-1.23-1.fc24
ovirt-engine-sdk-python-3.6.9.1-1.fc24
firefox-50.0-1.fc24
sassc-3.3.6-1.fc24
ovirt-engine-cli-3.6.9.2-1.fc24
auter-0.8-1.fc24
ovirt-engine-sdk-java-3.6.10.0-1.fc24
lensfun-0.3.2-3.fc24
libindicator-12.10.1-8.fc24
msmtp-1.6.6-1.fc24
irclog2html-2.15.2-1.fc24
erlang-parse_trans-3.0.0-1.fc24
erlang-setup-1.8.0-1.fc24
ocserv-0.11.6-1.fc24
flmsg-4.0.0-1.fc24
perl-Convert-Base64-0.001-1.fc24
erlang-cuttlefish-2.0.10-1.fc24
cinnamon-translations-3.2.0-1.fc24 cinnamon-3.2.1-1.fc24 nemo-extensions-3.2.0-1.fc24 nemo-3.2.0-1.fc24 cinnamon-control-center-3.2.0-1.fc24 muffin-3.2.0-1.fc24 cinnamon-session-3.2.0-1.fc24 cinnamon-settings-daemon-3.2.0-1.fc24 cinnamon-screensaver-3.2.0-2.fc24 cinnamon-menus-3.2.0-1.fc24 cinnamon-desktop-3.2.0-3.fc24 xapps-1.0.2-1.fc24 cjs-3.2.0-1.fc24
openvas-scanner-5.0.7-1.fc24
erlang-lager-3.2.4-1.fc24
perl-Crypt-IDEA-1.10-5.fc24
nfoview-1.20-1.fc23
php-akamai-open-edgegrid-client-0.6.0-1.fc23
drupal7-i18n-1.14-1.fc23
drupal7-chosen-2.0-1.fc23
drupal7-field_permissions-1.0-1.fc23
php-sentry-0.22.0-1.fc23
prosody-0.9.11-1.fc23
pidgin-groupchat-typing-notifications-0-2.git33a75f9.fc23
Push these 52 updates? (y/n) 
</pre>

Without looking into what is causing this, I'm going to guess it's an easy fix. Possibly a stray print statement…

Fix: #1123


Resolving patch:
diff --git a/bodhi/server/push.py b/bodhi/server/push.py
--- a/bodhi/server/push.py
+++ b/bodhi/server/push.py
@@ -21,13 +21,10 @@
 import glob
 
 from collections import defaultdict
-from pyramid.paster import get_appsettings
-from sqlalchemy import engine_from_config
 from sqlalchemy.sql import or_
 
 import bodhi.server.notifications
-from bodhi.server.util import transactional_session_maker
-from bodhi.server.models import Update, Base, UpdateRequest, Build, Release, ReleaseState
+from bodhi.server.models import Build, get_db_factory, Release, ReleaseState, Update, UpdateRequest
 
 
 @click.command()
@@ -40,13 +37,11 @@
 @click.option('--username', envvar='USERNAME', prompt=True)
 @click.option('--cert-prefix', default="shell",
               help="The prefix of a fedmsg cert used to sign the message.")
-@click.option('--config', help='Configuration file to use for database credentials',
-              default='/etc/bodhi/production.ini')
 @click.option('--staging', help='Use the staging bodhi instance',
               is_flag=True, default=False)
 @click.option('--resume', help='Resume one or more previously failed pushes',
               is_flag=True, default=False)
-def push(username, cert_prefix, config, **kwargs):
+def push(username, cert_prefix, **kwargs):
     staging = kwargs.pop('staging')
     resume = kwargs.pop('resume')
 
@@ -63,14 +58,9 @@ def push(username, cert_prefix, config, **kwargs):
             lockfiles[lockfile].append(update)
             locked_updates.append(update)
 
-    settings = get_appsettings(config)
-    engine = engine_from_config(settings, 'sqlalchemy.')
-    Base.metadata.create_all(engine)
-    db_factory = transactional_session_maker(engine)
-
     update_titles = None
 
-    with db_factory() as session:
+    with get_db_factory()() as session:
         updates = []
         # If we're resuming a push
         if resume:
@@ -82,7 +72,6 @@ def push(username, cert_prefix, config, **kwargs):
                 for update in lockfiles[lockfile]:
                     update = session.query(Update).filter(Update.title==update).first()
                     updates.append(update)
-                    click.echo(update)
         else:
             # Accept both comma and space separated request list
             requests = kwargs['request'].replace(',', ' ').split(' ')
@@ -118,18 +107,16 @@ def push(username, cert_prefix, config, **kwargs):
         for update in updates:
             print update.title
 
-        doit = raw_input('Push these %d updates? (y/n) ' % len(updates)).lower().strip()
-        if doit == 'y':
+        if updates:
+            click.confirm('Push these {:d} updates?'.format(len(updates)), abort=True)
             click.echo('\nLocking updates...')
             for update in updates:
                 update.locked = True
                 update.date_locked = datetime.utcnow()
-
-            update_titles = list([update.title for update in updates])
         else:
-            click.echo('\nAborting push')
-            raise Exception('Aborting push')
+            click.echo('\nThere are no updates to push.')
 
+        update_titles = list([update.title for update in updates])
 
     if update_titles:
         click.echo('\nSending masher.start fedmsg')


Test patch:
diff --git a/bodhi/tests/server/__init__.py b/bodhi/tests/server/__init__.py
--- a/bodhi/tests/server/__init__.py
+++ b/bodhi/tests/server/__init__.py
@@ -1,6 +1,8 @@
+from datetime import datetime, timedelta
+
 import mock
+import sqlalchemy
 
-from datetime import datetime, timedelta
 from bodhi.server.models import (
     Base,
     Bug,
@@ -21,6 +23,54 @@
 )
 
 
+def create_update(session, build_nvrs, release_name=u'F17'):
+    """
+    Use the given session to create and return an Update with the given iterable of build_nvrs. Each
+    build_nvr should be a string describing the name, version, and release for the build separated
+    by dashes. For example, build_nvrs might look like this:
+
+    (u'bodhi-2.3.3-1.fc24', u'python-fedora-atomic-composer-2016.3-1.fc24')
+
+    You can optionally pass a release_name to select a different release than the default F17, but
+    the release must already exist in the database.
+    """
+    release = session.query(Release).filter_by(name=release_name).one()
+    user = session.query(User).filter_by(name=u'guest').one()
+
+    builds = []
+    for nvr in build_nvrs:
+        name, version, rel = nvr.rsplit('-', 2)
+        try:
+            package = session.query(Package).filter_by(name=name).one()
+        except sqlalchemy.orm.exc.NoResultFound:
+            package = Package(name=name)
+            session.add(package)
+            user.packages.append(package)
+            testcase = TestCase(name=u'Wat')
+            session.add(testcase)
+            package.test_cases.append(testcase)
+
+        builds.append(Build(nvr=nvr, release=release, package=package))
+        session.add(builds[-1])
+
+        # Add a buildroot override for this build
+        expiration_date = datetime.utcnow()
+        expiration_date = expiration_date + timedelta(days=1)
+        override = BuildrootOverride(build=builds[-1], submitter=user,
+                                     notes=u'blah blah blah',
+                                     expiration_date=expiration_date)
+        session.add(override)
+
+    update = Update(
+        title=', '.join(build_nvrs), builds=builds, user=user, request=UpdateRequest.testing,
+        notes=u'Useful details!', release=release, date_submitted=datetime(1984, 11, 02),
+        requirements=u'rpmlint', stable_karma=3, unstable_karma=-3,
+        type=UpdateType.bugfix)
+    session.add(update)
+
+    return update
+
+
 def populate(db):
     user = User(name=u'guest')
     db.add(user)
@@ -44,23 +94,7 @@ def populate(db):
         override_tag=u'f17-override',
         branch=u'f17', state=ReleaseState.current)
     db.add(release)
-    pkg = Package(name=u'bodhi')
-    db.add(pkg)
-    user.packages.append(pkg)
-    build = Build(nvr=u'bodhi-2.0-1.fc17', release=release, package=pkg)
-    db.add(build)
-    testcase = TestCase(name=u'Wat')
-    db.add(testcase)
-    pkg.test_cases.append(testcase)
-    update = Update(
-        title=u'bodhi-2.0-1.fc17',
-        builds=[build], user=user,
-        request=UpdateRequest.testing,
-        notes=u'Useful details!', release=release,
-        date_submitted=datetime(1984, 11, 02),
-        requirements=u'rpmlint',
-        stable_karma=3, unstable_karma=-3,
-    )
+    update = create_update(db, [u'bodhi-2.0-1.fc17'])
     update.type = UpdateType.bugfix
     bug = Bug(bug_id=12345)
     db.add(bug)
@@ -83,14 +117,6 @@ def populate(db):
         update.assign_alias()
     db.add(update)
 
-    expiration_date = datetime.utcnow()
-    expiration_date = expiration_date + timedelta(days=1)
-
-    override = BuildrootOverride(build=build, submitter=user,
-                                 notes=u'blah blah blah',
-                                 expiration_date=expiration_date)
-    db.add(override)
-
     update.locked = True
 
     db.flush()
diff --git a/bodhi/tests/server/base.py b/bodhi/tests/server/base.py
--- a/bodhi/tests/server/base.py
+++ b/bodhi/tests/server/base.py
@@ -15,7 +15,7 @@
 This module contains a useful base test class that helps with common testing needs when testing
 bodhi.server modules.
 """
-
+from contextlib import contextmanager
 import unittest
 
 from sqlalchemy import create_engine
@@ -24,11 +24,9 @@
 from zope.sqlalchemy import ZopeTransactionExtension
 import transaction
 
-from bodhi.server import log
-from bodhi.tests.server import populate
-from bodhi.server.models import (
-    Base,
-)
+from bodhi.server import log, models
+from bodhi.tests.server import create_update, populate
+
 
 DB_PATH = 'sqlite://'
 DB_NAME = None
@@ -41,8 +39,8 @@ def setUp(self):
         self.Session = scoped_session(
             sessionmaker(bind=engine, extension=ZopeTransactionExtension(keep_session=True)))
         log.debug('Creating all models for %s' % engine)
-        Base.metadata.bind = engine
-        Base.metadata.create_all(engine)
+        models.Base.metadata.bind = engine
+        models.Base.metadata.create_all(engine)
         self.db = self.Session()
         transaction.begin()
         populate(self.db)
@@ -61,3 +59,44 @@ def tearDown(self):
         self.db.close()
         self.Session.remove()
         del self.sql_statements
+
+    def create_update(self, build_nvrs, release_name=u'F17'):
+        """
+        Create and return an Update with the given iterable of build_nvrs. Each build_nvr should be
+        a tuple of strings describing the name, version, and release for the build. For example,
+        build_nvrs might look like this:
+
+        ((u'bodhi', u'2.3.3', u'1.fc24'), (u'python-fedora-atomic-composer', u'2016.3', u'1.fc24'))
+
+        You can optionally pass a release_name to select a different release than the default F17,
+        but the release must already exist in the database.
+
+        This is a convenience wrapper around bodhi.tests.server.create_update so that tests can just
+        call self.create_update() and not have to pass self.db.
+        """
+        return create_update(self.db, build_nvrs, release_name)
+
+
+class TransactionalSessionMaker(object):
+    """
+    Mimic the behavior of bodhi.server.utils.TransactionalSessionMaker, but allow tests to inject
+    the test database Session.
+    """
+    def __init__(self, Session):
+        """
+        Store the Session for later retrieval.
+        """
+        self._Session = Session
+
+    @contextmanager
+    def __call__(self):
+        session = self._Session()
+        transaction.begin()
+        try:
+            yield session
+            transaction.commit()
+        except:
+            transaction.abort()
+            raise
+        finally:
+            session.close()
diff --git a/bodhi/tests/server/test_push.py b/bodhi/tests/server/test_push.py
--- a/bodhi/tests/server/test_push.py
+++ b/bodhi/tests/server/test_push.py
@@ -17,7 +17,10 @@
 
 from datetime import datetime
 
+from click.testing import CliRunner
 import click
+import mock
+import transaction
 
 from bodhi.server import push
 from bodhi.server.models import models
@@ -152,3 +155,608 @@ def test_unknown_release(self):
         with self.assertRaises(click.BadParameter) as ex:
             push._filter_releases(self.db, query, u'RELEASE WITH NO NAME')
             self.assertEqual(str(ex.exception), 'Unknown release: RELEASE WITH NO NAME')
+
+
+TEST_ABORT_PUSH_EXPECTED_OUTPUT = """Warning: bodhi-2.0-1.fc17 is locked but not in a push
+Warning: bodhi-2.0-1.fc17 has unsigned builds and has been skipped
+python-nose-1.3.7-11.fc17
+python-paste-deploy-1.5.2-8.fc17
+Push these 2 updates? [y/N]: n
+Aborted!
+"""
+
+TEST_BUILDS_FLAG_EXPECTED_OUTPUT = """ejabberd-16.09-4.fc17
+python-nose-1.3.7-11.fc17
+Push these 2 updates? [y/N]: y
+
+Locking updates...
+
+Sending masher.start fedmsg
+"""
+
+TEST_CERT_PREFIX_FLAG_EXPECTED_OUTPUT = """Warning: bodhi-2.0-1.fc17 is locked but not in a push
+Warning: bodhi-2.0-1.fc17 has unsigned builds and has been skipped
+python-nose-1.3.7-11.fc17
+python-paste-deploy-1.5.2-8.fc17
+Push these 2 updates? [y/N]: y
+
+Locking updates...
+
+Sending masher.start fedmsg
+"""
+
+TEST_LOCKED_UPDATES_EXPECTED_OUTPUT = """Warning: bodhi-2.0-1.fc17 is locked but not in a push
+Warning: bodhi-2.0-1.fc17 has unsigned builds and has been skipped
+python-nose-1.3.7-11.fc17
+python-paste-deploy-1.5.2-8.fc17
+Push these 2 updates? [y/N]: y
+
+Locking updates...
+
+Sending masher.start fedmsg
+"""
+
+TEST_LOCKED_UPDATES_NOT_IN_A_PUSH_EXPECTED_OUTPUT = """Warning: ejabberd-16.09-4.fc17 is locked but not in a push
+python-nose-1.3.7-11.fc17
+python-paste-deploy-1.5.2-8.fc17
+ejabberd-16.09-4.fc17
+Push these 3 updates? [y/N]: y
+
+Locking updates...
+
+Sending masher.start fedmsg
+"""
+
+TEST_NO_UPDATES_TO_PUSH_EXPECTED_OUTPUT = """Warning: python-nose-1.3.7-11.fc17 has unsigned builds and has been skipped
+Warning: python-paste-deploy-1.5.2-8.fc17 has unsigned builds and has been skipped
+Warning: bodhi-2.0-1.fc17 is locked but not in a push
+Warning: bodhi-2.0-1.fc17 has unsigned builds and has been skipped
+
+There are no updates to push.
+"""
+
+TEST_RELEASES_FLAG_EXPECTED_OUTPUT = """python-nose-1.3.7-11.fc25
+python-paste-deploy-1.5.2-8.fc26
+Push these 2 updates? [y/N]: y
+
+Locking updates...
+
+Sending masher.start fedmsg
+"""
+
+TEST_REQUEST_FLAG_EXPECTED_OUTPUT = """Warning: bodhi-2.0-1.fc17 is locked but not in a push
+Warning: bodhi-2.0-1.fc17 has unsigned builds and has been skipped
+python-paste-deploy-1.5.2-8.fc17
+Push these 1 updates? [y/N]: y
+
+Locking updates...
+
+Sending masher.start fedmsg
+"""
+
+TEST_RESUME_FLAG_EXPECTED_OUTPUT = """Resume /mnt/koji/mash/updates/MASHING-f17-updates? (y/n) ejabberd-16.09-4.fc17
+Push these 1 updates? [y/N]: y
+
+Locking updates...
+
+Sending masher.start fedmsg
+"""
+
+TEST_RESUME_HUMAN_SAYS_NO_EXPECTED_OUTPUT = """Resume /mnt/koji/mash/updates/MASHING-f17-testing? (y/n) Resume /mnt/koji/mash/updates/MASHING-f17-updates? (y/n) ejabberd-16.09-4.fc17
+Push these 1 updates? [y/N]: y
+
+Locking updates...
+
+Sending masher.start fedmsg
+"""
+
+TEST_STAGING_FLAG_EXPECTED_OUTPUT = """Warning: bodhi-2.0-1.fc17 is locked but not in a push
+Warning: bodhi-2.0-1.fc17 has unsigned builds and has been skipped
+python-nose-1.3.7-11.fc17
+python-paste-deploy-1.5.2-8.fc17
+Push these 2 updates? [y/N]: y
+
+Locking updates...
+
+Sending masher.start fedmsg
+"""
+
+TEST_UNSIGNED_UPDATES_SKIPPED_EXPECTED_OUTPUT = """Warning: python-nose-1.3.7-11.fc17 has unsigned builds and has been skipped
+Warning: bodhi-2.0-1.fc17 is locked but not in a push
+Warning: bodhi-2.0-1.fc17 has unsigned builds and has been skipped
+python-paste-deploy-1.5.2-8.fc17
+Push these 1 updates? [y/N]: y
+
+Locking updates...
+
+Sending masher.start fedmsg
+"""
+
+
+class TestPush(base.BaseTestCase):
+    """
+    This class contains tests for the push() function.
+    """
+    def setUp(self):
+        """
+        Make some updates that can be pushed.
+        """
+        super(TestPush, self).setUp()
+        python_nose = self.create_update([u'python-nose-1.3.7-11.fc17'])
+        python_paste_deploy = self.create_update([u'python-paste-deploy-1.5.2-8.fc17'])
+        # Make it so we have two builds to push out
+        python_nose.builds[0].signed = True
+        python_paste_deploy.builds[0].signed = True
+        self.db.flush()
+
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.publish')
+    def test_abort_push(self, publish):
+        """
+        Ensure that the push gets aborted if the user types 'n' when asked if they want to push.
+        """
+        cli = CliRunner()
+        transaction.commit()
+
+        with mock.patch('bodhi.server.push.get_db_factory',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            result = cli.invoke(push.push, ['--username', 'bowlofeggs'], input='n')
+
+        # The exit code is 1 when the push is aborted.
+        self.assertEqual(result.exit_code, 1)
+        self.assertEqual(result.output, TEST_ABORT_PUSH_EXPECTED_OUTPUT)
+        self.assertEqual(publish.call_count, 0)
+
+        # The updates should not be locked
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc17').one()
+        for u in [python_nose, python_paste_deploy]:
+            self.assertFalse(u.locked)
+            self.assertIsNone(u.date_locked)
+
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.publish')
+    def test_builds_flag(self, publish):
+        """
+        Assert correct operation when the --builds flag is given.
+        """
+        cli = CliRunner()
+        ejabberd = self.create_update([u'ejabberd-16.09-4.fc17'])
+        # Make it so we have three builds we could push out so that we can ask for and verify two
+        ejabberd.builds[0].signed = True
+        transaction.commit()
+
+        with mock.patch('bodhi.server.push.get_db_factory',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            result = cli.invoke(
+                push.push,
+                ['--username', 'bowlofeggs', '--builds',
+                 'python-nose-1.3.7-11.fc17,ejabberd-16.09-4.fc17'],
+                input='y')
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, TEST_BUILDS_FLAG_EXPECTED_OUTPUT)
+        publish.assert_called_once_with(
+            topic='masher.start',
+            msg={'updates': [u'ejabberd-16.09-4.fc17', u'python-nose-1.3.7-11.fc17'],
+                 'resume': False, 'agent': 'bowlofeggs'},
+            force=True)
+
+        ejabberd = self.db.query(models.Update).filter_by(title=u'ejabberd-16.09-4.fc17').one()
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc17').one()
+        for u in [ejabberd, python_nose]:
+            self.assertTrue(u.locked)
+            self.assertTrue(u.date_locked <= datetime.utcnow())
+        self.assertFalse(python_paste_deploy.locked)
+        self.assertIsNone(python_paste_deploy.date_locked)
+
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.init')
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.publish')
+    def test_cert_prefix_flag(self, publish, init):
+        """
+        Test correct operation when the --cert-prefix flag is used.
+        """
+        cli = CliRunner()
+        transaction.commit()
+
+        with mock.patch('bodhi.server.push.get_db_factory',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            result = cli.invoke(
+                push.push, ['--username', 'bowlofeggs', '--cert-prefix', 'some_prefix'], input='y')
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, TEST_CERT_PREFIX_FLAG_EXPECTED_OUTPUT)
+        init.assert_called_once_with(active=True, cert_prefix='some_prefix')
+        publish.assert_called_once_with(
+            topic='masher.start',
+            msg={'updates': ['python-nose-1.3.7-11.fc17', u'python-paste-deploy-1.5.2-8.fc17'],
+                 'resume': False, 'agent': 'bowlofeggs'},
+            force=True)
+
+    @mock.patch('bodhi.server.push.file')
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.publish')
+    @mock.patch('bodhi.server.push.glob.glob',
+                return_value=['/mnt/koji/mash/updates/MASHING-f17-updates'])
+    @mock.patch('bodhi.server.push.json.load', return_value={'updates': ['ejabberd-16.09-4.fc17']})
+    def test_locked_updates(self, load, glob, publish, mock_file):
+        """
+        Test correct operation when there are some locked updates.
+        """
+        cli = CliRunner()
+        # Let's mark ejabberd as locked and already in a push. It should get silently ignored.
+        ejabberd = self.create_update([u'ejabberd-16.09-4.fc17'])
+        ejabberd.builds[0].signed = True
+        ejabberd.locked = True
+        ejabberd.date_locked = datetime.utcnow()
+        transaction.commit()
+
+        with mock.patch('bodhi.server.push.get_db_factory',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            result = cli.invoke(push.push, ['--username', 'bowlofeggs'], input='y')
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, TEST_LOCKED_UPDATES_EXPECTED_OUTPUT)
+        glob.assert_called_once_with('/mnt/koji/mash/updates/MASHING-*')
+        publish.assert_called_once_with(
+            topic='masher.start',
+            msg={'updates': ['python-nose-1.3.7-11.fc17', 'python-paste-deploy-1.5.2-8.fc17'],
+                 'resume': False, 'agent': 'bowlofeggs'},
+            force=True)
+        mock_file.assert_called_once_with('/mnt/koji/mash/updates/MASHING-f17-updates')
+
+        ejabberd = self.db.query(models.Update).filter_by(title=u'ejabberd-16.09-4.fc17').one()
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc17').one()
+        for u in [ejabberd, python_nose, python_paste_deploy]:
+            self.assertTrue(u.locked)
+            self.assertTrue(u.date_locked <= datetime.utcnow())
+
+    @mock.patch('bodhi.server.push.file')
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.publish')
+    @mock.patch('bodhi.server.push.glob.glob',
+                return_value=['/mnt/koji/mash/updates/MASHING-f17-updates'])
+    @mock.patch('bodhi.server.push.json.load', return_value={'updates': ['bodhi-2.0-1.fc17']})
+    def test_locked_updates_not_in_a_push(self, load, glob, publish, mock_file):
+        """
+        Test correct operation when there are some locked updates that aren't in a push.
+        """
+        cli = CliRunner()
+        # Let's mark ejabberd as locked but not already in a push. It should print a warning but
+        # still get pushed.
+        ejabberd = self.create_update([u'ejabberd-16.09-4.fc17'])
+        ejabberd.builds[0].signed = True
+        ejabberd.locked = True
+        ejabberd.date_locked = datetime.utcnow()
+        transaction.commit()
+
+        with mock.patch('bodhi.server.push.get_db_factory',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            result = cli.invoke(push.push, ['--username', 'bowlofeggs'], input='y')
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, TEST_LOCKED_UPDATES_NOT_IN_A_PUSH_EXPECTED_OUTPUT)
+        glob.assert_called_once_with('/mnt/koji/mash/updates/MASHING-*')
+        publish.assert_called_once_with(
+            topic='masher.start',
+            msg={'updates': ['python-nose-1.3.7-11.fc17', 'python-paste-deploy-1.5.2-8.fc17',
+                             'ejabberd-16.09-4.fc17'],
+                 'resume': False, 'agent': 'bowlofeggs'},
+            force=True)
+        mock_file.assert_called_once_with('/mnt/koji/mash/updates/MASHING-f17-updates')
+
+        ejabberd = self.db.query(models.Update).filter_by(title=u'ejabberd-16.09-4.fc17').one()
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc17').one()
+        for u in [ejabberd, python_nose, python_paste_deploy]:
+            self.assertTrue(u.locked)
+            self.assertTrue(u.date_locked <= datetime.utcnow())
+
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.publish')
+    def test_no_updates_to_push(self, publish):
+        """
+        If there are no updates to push, no push message should get sent.
+        """
+        cli = CliRunner()
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc17').one()
+        python_nose.builds[0].signed = False
+        python_paste_deploy.builds[0].signed = False
+        transaction.commit()
+
+        with mock.patch('bodhi.server.push.get_db_factory',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            result = cli.invoke(push.push, ['--username', 'bowlofeggs'], input='y')
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, TEST_NO_UPDATES_TO_PUSH_EXPECTED_OUTPUT)
+        self.assertEqual(publish.call_count, 0)
+
+        # The updates should not be locked
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc17').one()
+        for u in [python_nose, python_paste_deploy]:
+            self.assertFalse(u.locked)
+            self.assertIsNone(u.date_locked)
+
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.publish')
+    def test_releases_flag(self, publish):
+        """
+        Assert correct operation from the --releases flag.
+        """
+        f25 = models.Release(
+            name=u'F25', long_name=u'Fedora 25',
+            id_prefix=u'FEDORA', version=u'25',
+            dist_tag=u'f25', stable_tag=u'f25-updates',
+            testing_tag=u'f25-updates-testing',
+            candidate_tag=u'f25-updates-candidate',
+            pending_signing_tag=u'f25-updates-testing-signing',
+            pending_testing_tag=u'f25-updates-testing-pending',
+            pending_stable_tag=u'f25-updates-pending',
+            override_tag=u'f25-override',
+            branch=u'f25', state=models.ReleaseState.current)
+        f26 = models.Release(
+            name=u'F26', long_name=u'Fedora 26',
+            id_prefix=u'FEDORA', version=u'26',
+            dist_tag=u'f26', stable_tag=u'f26-updates',
+            testing_tag=u'f26-updates-testing',
+            candidate_tag=u'f26-updates-candidate',
+            pending_signing_tag=u'f26-updates-testing-signing',
+            pending_testing_tag=u'f26-updates-testing-pending',
+            pending_stable_tag=u'f26-updates-pending',
+            override_tag=u'f26-override',
+            branch=u'f26', state=models.ReleaseState.current)
+        self.db.add(f25)
+        self.db.add(f26)
+        # Let's make an update for each release
+        python_nose = self.create_update([u'python-nose-1.3.7-11.fc25'], u'F25')
+        python_paste_deploy = self.create_update([u'python-paste-deploy-1.5.2-8.fc26'], u'F26')
+        python_nose.builds[0].signed = True
+        python_paste_deploy.builds[0].signed = True
+        transaction.commit()
+        cli = CliRunner()
+
+        with mock.patch('bodhi.server.push.get_db_factory',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            # We will specify that we want F25 and F26, which should exclude the F17 updates we've
+            # been pushing in all the other tests. We'll leave the F off of 26 and lowercase the f
+            # on 25 to make sure it's flexible.
+            result = cli.invoke(push.push, ['--username', 'bowlofeggs', '--releases', 'f25,26'],
+                                input='y')
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, TEST_RELEASES_FLAG_EXPECTED_OUTPUT)
+        publish.assert_called_once_with(
+            topic='masher.start',
+            msg={'updates': ['python-nose-1.3.7-11.fc25', 'python-paste-deploy-1.5.2-8.fc26'],
+                 'resume': False, 'agent': 'bowlofeggs'},
+            force=True)
+
+        # The Fedora 17 updates should not have been locked.
+        f17_python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        f17_python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc17').one()
+        self.assertFalse(f17_python_nose.locked)
+        self.assertIsNone(f17_python_nose.date_locked)
+        self.assertFalse(f17_python_paste_deploy.locked)
+        self.assertIsNone(f17_python_paste_deploy.date_locked)
+        # The new updates should both be locked.
+        f25_python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc25').one()
+        f26_python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc26').one()
+        self.assertTrue(f25_python_nose.locked)
+        self.assertTrue(f25_python_nose.date_locked <= datetime.utcnow())
+        self.assertTrue(f26_python_paste_deploy.locked)
+        self.assertTrue(f26_python_paste_deploy.date_locked <= datetime.utcnow())
+
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.publish')
+    def test_request_flag(self, publish):
+        """
+        Assert that the --request flag works correctly.
+        """
+        cli = CliRunner()
+        # Let's mark nose as a stable request so it gets excluded when we request a testing update.
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_nose.request = models.UpdateRequest.stable
+        transaction.commit()
+
+        with mock.patch('bodhi.server.push.get_db_factory',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            result = cli.invoke(push.push, ['--username', 'bowlofeggs', '--request', 'testing'],
+                                input='y')
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, TEST_REQUEST_FLAG_EXPECTED_OUTPUT)
+        publish.assert_called_once_with(
+            topic='masher.start',
+            msg={'updates': ['python-paste-deploy-1.5.2-8.fc17'],
+                 'resume': False, 'agent': 'bowlofeggs'},
+            force=True)
+
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc17').one()
+        self.assertFalse(python_nose.locked)
+        self.assertIsNone(python_nose.date_locked)
+        self.assertTrue(python_paste_deploy.locked)
+        self.assertTrue(python_paste_deploy.date_locked <= datetime.utcnow())
+
+    @mock.patch('bodhi.server.push.file')
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.publish')
+    @mock.patch('bodhi.server.push.glob.glob',
+                return_value=['/mnt/koji/mash/updates/MASHING-f17-updates'])
+    @mock.patch('bodhi.server.push.json.load', return_value={'updates': [u'ejabberd-16.09-4.fc17']})
+    def test_resume_flag(self, load, glob, publish, mock_file):
+        """
+        Test correct operation when the --resume flag is given.
+        """
+        cli = CliRunner()
+        # Let's mark ejabberd as locked and already in a push. Since we are resuming, it should be
+        # the only package that gets included.
+        ejabberd = self.create_update([u'ejabberd-16.09-4.fc17'])
+        ejabberd.builds[0].signed = True
+        ejabberd.locked = True
+        ejabberd.date_locked = datetime.utcnow()
+        transaction.commit()
+
+        with mock.patch('bodhi.server.push.get_db_factory',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            result = cli.invoke(push.push, ['--username', 'bowlofeggs', '--resume'], input='y\ny')
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, TEST_RESUME_FLAG_EXPECTED_OUTPUT)
+        glob.assert_called_once_with('/mnt/koji/mash/updates/MASHING-*')
+        publish.assert_called_once_with(
+            topic='masher.start',
+            msg={'updates': ['ejabberd-16.09-4.fc17'],
+                 'resume': True, 'agent': 'bowlofeggs'},
+            force=True)
+        mock_file.assert_called_once_with('/mnt/koji/mash/updates/MASHING-f17-updates')
+
+        ejabberd = self.db.query(models.Update).filter_by(title=u'ejabberd-16.09-4.fc17').one()
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc17').one()
+        # ejabberd should be locked still
+        self.assertTrue(ejabberd.locked)
+        self.assertTrue(ejabberd.date_locked <= datetime.utcnow())
+        # The other packages should have been left alone
+        for u in [python_nose, python_paste_deploy]:
+            self.assertFalse(u.locked)
+            self.assertIsNone(u.date_locked)
+
+    @mock.patch('bodhi.server.push.file')
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.publish')
+    @mock.patch('bodhi.server.push.glob.glob',
+                return_value=['/mnt/koji/mash/updates/MASHING-f17-testing',
+                              '/mnt/koji/mash/updates/MASHING-f17-updates'])
+    @mock.patch('bodhi.server.push.json.load',
+                side_effect=[{'updates': [u'python-nose-1.3.7-11.fc17']},
+                             {'updates': [u'ejabberd-16.09-4.fc17']}])
+    def test_resume_human_says_no(self, load, glob, publish, mock_file):
+        """
+        Test correct operation when the --resume flag is given but the human says they don't want to
+        resume one of the lockfiles.
+        """
+        cli = CliRunner()
+        # Let's mark ejabberd as locked and already in a push. Since we are resuming and since we
+        # will decline pushing the first time we are asked, it should be the only package that gets
+        # included.
+        ejabberd = self.create_update([u'ejabberd-16.09-4.fc17'])
+        ejabberd.builds[0].signed = True
+        ejabberd.locked = True
+        ejabberd.date_locked = datetime.utcnow()
+        transaction.commit()
+
+        with mock.patch('bodhi.server.push.get_db_factory',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            result = cli.invoke(push.push, ['--username', 'bowlofeggs', '--resume'],
+                                input='n\ny\ny')
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, TEST_RESUME_HUMAN_SAYS_NO_EXPECTED_OUTPUT)
+        glob.assert_called_once_with('/mnt/koji/mash/updates/MASHING-*')
+        publish.assert_called_once_with(
+            topic='masher.start',
+            msg={'updates': ['ejabberd-16.09-4.fc17'],
+                 'resume': True, 'agent': 'bowlofeggs'},
+            force=True)
+        mock_file.assert_any_call('/mnt/koji/mash/updates/MASHING-f17-testing')
+        mock_file.assert_any_call('/mnt/koji/mash/updates/MASHING-f17-updates')
+
+        ejabberd = self.db.query(models.Update).filter_by(title=u'ejabberd-16.09-4.fc17').one()
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc17').one()
+        # ejabberd should be locked still
+        self.assertTrue(ejabberd.locked)
+        self.assertTrue(ejabberd.date_locked <= datetime.utcnow())
+        # The other packages should have been left alone
+        for u in [python_nose, python_paste_deploy]:
+            self.assertFalse(u.locked)
+            self.assertIsNone(u.date_locked)
+
+    @mock.patch('bodhi.server.push.file')
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.publish')
+    @mock.patch('bodhi.server.push.glob.glob',
+                return_value=['/mnt/koji/mash/updates/MASHING-f17-updates'])
+    @mock.patch('bodhi.server.push.json.load', return_value={'updates': [u'ejabberd-16.09-4.fc17']})
+    def test_staging_flag(self, load, glob, publish, mock_file):
+        """
+        Test correct operation when the --staging flag is given. The main thing that matters is that
+        the glob call happens on a different directory.
+        """
+        cli = CliRunner()
+        transaction.commit()
+
+        with mock.patch('bodhi.server.push.get_db_factory',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            result = cli.invoke(push.push, ['--username', 'bowlofeggs', '--staging'], input='y')
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, TEST_STAGING_FLAG_EXPECTED_OUTPUT)
+        glob.assert_called_once_with('/var/cache/bodhi/mashing/MASHING-*')
+        publish.assert_called_once_with(
+            topic='masher.start',
+            msg={'updates': ['python-nose-1.3.7-11.fc17', u'python-paste-deploy-1.5.2-8.fc17'],
+                 'resume': False, 'agent': 'bowlofeggs'},
+            force=True)
+        mock_file.assert_called_once_with('/mnt/koji/mash/updates/MASHING-f17-updates')
+
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc17').one()
+        # The packages should be locked
+        for u in [python_nose, python_paste_deploy]:
+            self.assertTrue(u.locked)
+            self.assertTrue(u.date_locked <= datetime.utcnow())
+
+    @mock.patch('bodhi.server.push.bodhi.server.notifications.publish')
+    def test_unsigned_updates_skipped(self, publish):
+        """
+        Unsigned updates should get skipped.
+        """
+        cli = CliRunner()
+        # Let's mark nose unsigned so it gets skipped.
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_nose.builds[0].signed = False
+        transaction.commit()
+
+        with mock.patch('bodhi.server.push.get_db_factory',
+                        return_value=base.TransactionalSessionMaker(self.Session)):
+            result = cli.invoke(push.push, ['--username', 'bowlofeggs'], input='y')
+
+        self.assertEqual(result.exit_code, 0)
+        self.assertEqual(result.output, TEST_UNSIGNED_UPDATES_SKIPPED_EXPECTED_OUTPUT)
+        publish.assert_called_once_with(
+            topic='masher.start',
+            msg={'updates': ['python-paste-deploy-1.5.2-8.fc17'],
+                 'resume': False, 'agent': 'bowlofeggs'},
+            force=True)
+
+        python_nose = self.db.query(models.Update).filter_by(
+            title=u'python-nose-1.3.7-11.fc17').one()
+        python_paste_deploy = self.db.query(models.Update).filter_by(
+            title=u'python-paste-deploy-1.5.2-8.fc17').one()
+        self.assertFalse(python_nose.locked)
+        self.assertIsNone(python_nose.date_locked)
+        self.assertTrue(python_paste_deploy.locked)
+        self.assertTrue(python_paste_deploy.date_locked <= datetime.utcnow())