autoland: write integration test for fake reviewboard service (bug 1337517) r?smacleod draft
authorMāris Fogels <mars@mozilla.com>
Tue, 07 Feb 2017 16:34:57 -0500
changeset 228 9b59c9562abe8b3461a936eaf155098625e743a5
parent 227 323798fd77ee2546f57fede8bb5dc82960ccb156
child 229 812e64dc7d145ec47c578f01bf235578cbfb098b
push id116
push usermfogels@mozilla.com
push dateThu, 23 Feb 2017 15:58:42 +0000
reviewerssmacleod
bugs1337517
autoland: write integration test for fake reviewboard service (bug 1337517) r?smacleod Add a fake reviewboard service to the test suite and use it to test repo URL handling. The fake reviewboard is filled with junk data until we can get some real test data to work with. MozReview-Commit-ID: AXjkjERCAbJ
autoland/webapi/autolandweb/routes.py
autoland/webapi/autolandweb/testing.py
autoland/webapi/tests/test_repo_check.py
--- a/autoland/webapi/autolandweb/routes.py
+++ b/autoland/webapi/autolandweb/routes.py
@@ -1,27 +1,52 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import tornado.httpclient
 import tornado.web
 
 from autolandweb.dockerflow import DOCKERFLOW_ROUTES
 from autolandweb.series import get_series_status
 
 
 class MainHandler(tornado.web.RequestHandler):
     def get(self):
         self.write('Hello, from Autoland')
 
 
 class ReposHandler(tornado.web.RequestHandler):
     """Handler for repositories."""
 
-    def get(self, repo=None):
-        pass
+    async def get(self, repo=None):
+        if repo is None:
+            return
+
+        http = tornado.httpclient.AsyncHTTPClient()
+        # FIXME: We don't validate the user input here!  Security problem?
+        repo_url = self.settings['reviewboard_url'] + '/repos/' + repo
+        # FIXME: Should the user agent be configurable or a constant?
+        response = await http.fetch(
+            repo_url,
+            headers={'Accept': 'application/json'},
+            user_agent='autoland tornado AsyncHTTPClient',
+            raise_error=False
+        )
+
+        # Handle HTTP response codes.  3XX codes have already been handled
+        # and followed by the tornado HTTP client.
+        if response.code == 200:
+            self.set_status(200)
+        elif response.code == 404:
+            self.set_status(404)
+            self.write({'error': 'Repo not found'})
+        else:
+            # Everything not 2XX or 404 is an exception.
+            response.rethrow()
 
 
 class SeriesHandler(tornado.web.RequestHandler):
     """Handler for series'."""
 
     async def get(self, repo, series=None):
         if series is None:
             self.write({})
new file mode 100644
--- /dev/null
+++ b/autoland/webapi/autolandweb/testing.py
@@ -0,0 +1,64 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+"""
+Test helpers.
+"""
+
+from collections import namedtuple
+
+import requests
+
+
+class MountebankClient:
+    def __init__(self, host, port=2525, imposter_port=4000):
+        self.host = host
+        self.port = port
+        self.imposter_port = imposter_port
+
+    @property
+    def imposters_admin_url(self):
+        return self.get_endpoint_with_port(self.port, '/imposters')
+
+    @property
+    def stub_baseurl(self):
+        return self.get_endpoint_with_port(self.imposter_port)
+
+    def get_endpoint(self, path=''):
+        """Construct a URL for the imposter service with optional path."""
+        return self.get_endpoint_with_port(self.imposter_port, path)
+
+    def get_endpoint_with_port(self, port, path=''):
+        """Construct a service endpoint URL with port and optional path."""
+        return 'http://{0}:{1}{2}'.format(self.host, port, path)
+
+    def create_imposter(self, imposter_json):
+        """Take a dict and turn it into a service stub."""
+        response = requests.post(self.imposters_admin_url, json=imposter_json)
+        if response.status_code != 201:
+            raise RuntimeError(
+                "mountebank imposter creation failed: {0} {1}".
+                format(response.status_code, response.content)
+            )
+
+    def create_stub(self, stub_json):
+        """Create a http stub using the default imposter port."""
+        self.create_imposter(
+            {
+                'port': self.imposter_port,
+                'protocol': 'http',
+                'stubs': stub_json
+            }
+        )
+
+    def reset_imposters(self):
+        """Delete all imposters."""
+        requests.delete(self.imposters_admin_url)
+
+    def get_requests(self):
+        """Return a list of requests made to the imposter."""
+        url = self.imposters_admin_url + '/' + str(self.imposter_port)
+        return requests.get(url).json().get('requests')
+
+
+MBHostInfo = namedtuple('MBHostInfo', 'ip adminport imposterport')
new file mode 100644
--- /dev/null
+++ b/autoland/webapi/tests/test_repo_check.py
@@ -0,0 +1,141 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+"""
+Test repository handling logic.
+"""
+
+from autolandweb.server import make_app
+from autolandweb.testing import MountebankClient
+
+import pytest
+
+# FIXME: need to get pycharm test runner to add /app to the container PYTHONPATH # noqa
+import sys
+sys.path.insert(0, '/app')
+
+
+class FakeReviewBoard:
+    def __init__(self, mountebank_client):
+        self.mountebank = mountebank_client
+
+    @property
+    def url(self):
+        # Copied from the project docker-compose.yml file.
+        return 'http://mountebank:' + str(self.mountebank.imposter_port)
+
+    def create_repo(self, name, repo_info):
+        """Create a repo in the fake reviewboard server."""
+        repo_path = '/repos/' + name
+        self.mountebank.create_stub(
+            [
+                # 200 for our repo path
+                {
+                    "predicates":
+                    [{
+                        "equals": {
+                            "method": "GET",
+                            "path": repo_path
+                        }
+                    }],
+                    "responses": [
+                        {
+                            "is": {
+                                "statusCode": 200,
+                                "headers": {
+                                    "Content-Type": "application/json"
+                                },
+                                "body": repo_info
+                            }
+                        }
+                    ]
+                },
+                # 404 everything else
+                {
+                    "predicates": [{
+                        "not": {
+                            "equals": {
+                                "path": repo_path
+                            }
+                        }
+                    }],
+                    "responses": [{
+                        "is": {
+                            "statusCode": 404
+                        }
+                    }]
+                }
+            ]
+        )
+
+
+@pytest.fixture
+def app(reviewboard):
+    """Returns the tornado.Application instance we'll be testing against.
+
+    Required for pytest-tornado to function.
+    """
+    return make_app(
+        reviewboard_url=reviewboard.url,
+        version_data={
+            'commit': None,
+            'version': 'test',
+            'source': 'https://hg.mozilla.org/automation/conduit',
+            'build': 'test',
+        }
+    )
+
+
+@pytest.fixture
+def api_root(base_url):
+    return base_url + "/api/v1"
+
+
+@pytest.fixture(scope='session')
+def mountebank():
+    # The docker-compose internal DNS entry for the mountebank container
+    mountebank_host = "mountebank"
+    # Lifted from the docker-compose file
+    mountebank_admin_port = 2525
+    mountebank_imposter_port = 4000
+
+    return MountebankClient(
+        mountebank_host, mountebank_admin_port, mountebank_imposter_port
+    )
+
+
+@pytest.fixture
+def reviewboard(request, mountebank):
+    # NOTE: comment out the line below if you want mountebank to save your
+    # requests and responses for inspection after the test suite completes.
+    # You can manually clean up the imposters afterwards by sending HTTP
+    # DELETE to the exposed mountebank admin port, documented in
+    # docker-compose.yml, or by restarting the mountebank container. See
+    # http://www.mbtest.org/docs/api/stubs for details.
+    request.addfinalizer(mountebank.reset_imposters)
+    return FakeReviewBoard(mountebank)
+
+
+@pytest.mark.gen_test
+async def test_return_info_for_valid_repo(http_client, api_root, reviewboard):
+    # Arrange
+    repo_name = 'mycoolrepo'
+    repo_info = {'repo': {'name': repo_name}}
+    reviewboard.create_repo(repo_name, repo_info)
+
+    # Act
+    repo_url = api_root + '/repos/' + repo_name
+    response = await http_client.fetch(repo_url)
+
+    # Assert
+    assert response.code == 200
+
+
+@pytest.mark.gen_test
+async def test_return_404_if_repo_not_in_reviewboard(
+    http_client, api_root, reviewboard
+):
+    repo_url = api_root + "/repos/zabumafu"
+    reviewboard.create_repo("movealong", {})
+    response = await http_client.fetch(repo_url, raise_error=False)
+    assert response.code == 404