Repository: fedora-infra/bodhi

Issue:
[S] Message consumer: Move update from side-tag to testing
# Story:

As a user of multi-build gating,

I want a Bodhi message consumer to change the status of an update created for a side-tag to `testing` when all of its builds are signed by Robosignatory (i.e. tagged into the `*-testing` tag corresponding to its side-tag) and announce it via the message bus,

so that automated testing can commence.

# Background

pypingou's multi-build update workflow diagram \[[download](https://user-images.githubusercontent.com/820624/63262799-34940480-c287-11e9-9594-e177ce7e6d4d.png)\]:

![Multi-builds - Rawhide Package Gating Flow.png](https://user-images.githubusercontent.com/820624/63262799-34940480-c287-11e9-9594-e177ce7e6d4d.png)

This card corresponds to the "Move update to status: testing" step half-way down in the Bodhi column.


Discussion:

Resolving patch:
diff --git a/bodhi/server/consumers/signed.py b/bodhi/server/consumers/signed.py
--- a/bodhi/server/consumers/signed.py
+++ b/bodhi/server/consumers/signed.py
@@ -25,8 +25,9 @@
 import logging
 
 import fedora_messaging
+from sqlalchemy import func
 
-from bodhi.server.models import Build
+from bodhi.server.models import Build, UpdateStatus
 from bodhi.server.util import transactional_session_maker
 
 log = logging.getLogger('bodhi')
@@ -87,9 +88,15 @@ def __call__(self, message: fedora_messaging.api.Message):
                 log.info('Build is not assigned to release, skipping')
                 return
 
-            if build.release.pending_testing_tag != tag:
-                log.info("Tag is not pending_testing tag, skipping")
-                return
+            if build.update.from_tag:
+                koji_testing_tag = build.release.get_testing_side_tag(build.update.from_tag)
+                if tag != koji_testing_tag:
+                    log.info("Tag is not testing side tag, skipping")
+                    return
+            else:
+                if build.release.pending_testing_tag != tag:
+                    log.info("Tag is not pending_testing tag, skipping")
+                    return
 
             if build.signed:
                 log.info("Build was already marked as signed (maybe a duplicate message)")
@@ -101,3 +108,12 @@ def __call__(self, message: fedora_messaging.api.Message):
             log.info("Build has been signed, marking")
             build.signed = True
             log.info("Build %s has been marked as signed" % build_nvr)
+
+            # If every build in update is signed change status to testing
+            if build.update.from_tag and build.update.signed():
+                log.info("Every build in update is signed, set status to testing")
+
+                build.update.status = UpdateStatus.testing
+                build.update.date_testing = func.current_timestamp()
+                build.update.request = None
+                log.info(f"Update {build.update.display_name} status has been set to testing")
diff --git a/bodhi/server/models.py b/bodhi/server/models.py
--- a/bodhi/server/models.py
+++ b/bodhi/server/models.py
@@ -2390,10 +2390,12 @@ def signed(self):
         signed, or if the associated :class:`Release` does not have a ``pending_signing_tag``
         defined. Otherwise, it will return ``False``.
 
+        If the update is created ``from_tag`` always check if every build is signed.
+
         Returns:
             bool: ``True`` if the update is signed, ``False`` otherwise.
         """
-        if not self.release.pending_signing_tag:
+        if not self.release.pending_signing_tag and not self.from_tag:
             return True
         return all([build.signed for build in self.builds])
 


Test patch:
diff --git a/bodhi/tests/server/consumers/test_signed.py b/bodhi/tests/server/consumers/test_signed.py
--- a/bodhi/tests/server/consumers/test_signed.py
+++ b/bodhi/tests/server/consumers/test_signed.py
@@ -23,6 +23,7 @@
 from fedora_messaging.api import Message
 
 from bodhi.server.consumers import signed
+from bodhi.server.models import UpdateStatus
 
 
 class TestSignedHandlerConsume(unittest.TestCase):
@@ -43,6 +44,20 @@ def setUp(self):
                 'release': '1.fc26'
             },
         )
+        self.sample_side_tag_message = Message(
+            topic='',
+            body={
+                'build_id': 442562,
+                'name': 'colord',
+                'tag_id': 214,
+                'instance': 's390',
+                'tag': 'f30-side-tag-testing',
+                'user': 'sharkcz',
+                'version': '1.3.4',
+                'owner': 'sharkcz',
+                'release': '1.fc26'
+            },
+        )
         self.handler = signed.SignedHandler()
 
     @mock.patch('bodhi.server.consumers.signed.Build')
@@ -50,6 +65,9 @@ def test_consume(self, mock_build_model):
         """Assert that messages marking the build as signed updates the database"""
         build = mock_build_model.get.return_value
         build.signed = False
+        update = mock.MagicMock()
+        update.from_tag = None
+        build.update = update
         build.release.pending_testing_tag = self.sample_message.body["tag"]
 
         self.handler(self.sample_message)
@@ -62,6 +80,9 @@ def test_consume_not_pending_testing_tag(self, mock_build_model):
         """
         build = mock_build_model.get.return_value
         build.signed = False
+        update = mock.MagicMock()
+        update.from_tag = None
+        build.update = update
         build.release.pending_testing_tag = "some tag that isn't pending testing"
 
         self.handler(self.sample_message)
@@ -74,6 +95,9 @@ def test_consume_no_release(self, mock_build_model):
         """
         build = mock_build_model.get.return_value
         build.signed = False
+        update = mock.MagicMock()
+        update.from_tag = None
+        build.update = update
         build.release = None
 
         self.handler(self.sample_message)
@@ -95,8 +119,72 @@ def test_consume_duplicate(self, mock_build_model, mock_log):
         build = mock_build_model.get.return_value
         build.release.pending_testing_tag = self.sample_message.body["tag"]
         build.signed = True
+        update = mock.MagicMock()
+        update.from_tag = None
+        build.update = update
 
         self.handler(self.sample_message)
         mock_log.info.assert_called_with(
             "Build was already marked as signed (maybe a duplicate message)")
         self.assertEqual(mock_log.info.call_count, 2)
+
+    @mock.patch('bodhi.server.consumers.signed.log')
+    @mock.patch('bodhi.server.consumers.signed.Build')
+    def test_consume_from_tag_wrong_tag(self, mock_build_model, mock_log):
+        """
+        Assert that messages about builds from side tag updates are skipped
+        when tag is not correct.
+        """
+        build = mock_build_model.get.return_value
+        build.signed = False
+        build.release = mock.MagicMock()
+        update = mock.MagicMock()
+        update.from_tag = "f30-side-tag-unknown"
+        build.update = update
+
+        self.handler(self.sample_side_tag_message)
+        mock_log.info.assert_called_with(
+            "Tag is not testing side tag, skipping")
+        self.assertEqual(mock_log.info.call_count, 2)
+
+    @mock.patch('bodhi.server.consumers.signed.Build')
+    def test_consume_from_tag_not_signed(self, mock_build_model):
+        """
+        Assert that update created from tag is not changed to status testing till
+        every build is signed when message is received.
+        """
+        build = mock_build_model.get.return_value
+        build.signed = False
+        build.release = mock.MagicMock()
+        build.release.get_testing_side_tag.return_value = "f30-side-tag-testing"
+        update = mock.MagicMock()
+        update.from_tag = "f30-side-tag"
+        update.signed.return_value = False
+        update.status = UpdateStatus.pending
+        build.update = update
+
+        self.handler(self.sample_side_tag_message)
+        self.assertEqual(build.signed, True)
+        self.assertEqual(update.status, UpdateStatus.pending)
+
+    @mock.patch('bodhi.server.consumers.signed.Build')
+    def test_consume_from_tag(self, mock_build_model):
+        """
+        Assert that update created from tag is handled correctly when message
+        is received.
+        Update status is changed to testing and corresponding message is sent.
+        """
+        build = mock_build_model.get.return_value
+        build.signed = False
+        build.release = mock.MagicMock()
+        build.release.get_testing_side_tag.return_value = "f30-side-tag-testing"
+        update = mock.MagicMock()
+        update.from_tag = "f30-side-tag"
+        update.signed.return_value = True
+        update.status = UpdateStatus.pending
+        build.update = update
+
+        self.handler(self.sample_side_tag_message)
+        self.assertEqual(build.signed, True)
+        self.assertEqual(build.update.request, None)
+        self.assertEqual(update.status, UpdateStatus.testing)
diff --git a/bodhi/tests/server/test_models.py b/bodhi/tests/server/test_models.py
--- a/bodhi/tests/server/test_models.py
+++ b/bodhi/tests/server/test_models.py
@@ -1903,6 +1903,15 @@ def test_release_without_pending_signing_tag(self):
 
         self.assertTrue(update.signed)
 
+    def test_from_tag_update(self):
+        """If the update's release doesn't have a pending_signing_tag, it should return True."""
+        update = model.Update.query.first()
+        update.builds[0].signed = False
+        update.from_tag = 'f30-side-tag'
+        update.release.pending_signing_tag = ''
+
+        self.assertFalse(update.signed)
+
 
 class TestUpdateTestGatingPassed(BaseTestCase):
     """Test the Update.test_gating_passed() method."""