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
--- 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