commit-index: Mountebank tests for testing getting commit data and raw diff from hg server (bug 1349673) r?mars draft
authorDavid Lawrence <dkl@mozilla.com>
Tue, 28 Mar 2017 13:15:09 -0400
changeset 5639 d58a510ff7859726cb9998c502661b2473572ade
parent 5638 841a6a870a60210f0cc31e06f5606d96834037fc
push id228
push userdlawrence@mozilla.com
push dateWed, 05 Apr 2017 21:13:59 +0000
reviewersmars
bugs1349673
commit-index: Mountebank tests for testing getting commit data and raw diff from hg server (bug 1349673) r?mars - Update iterations.py to use mercurial.py to access data from hg server - mountebank stubs tests getting commit data as well as raw diffs from fake hg server MozReview-Commit-ID: Cxnu0o0MvqP
commitindex/commitindex/api/iterations.py
commitindex/commitindex/commits/mercurial.py
commitindex/commitindex/reviews/bugzilla.py
commitindex/tests/test_hg_server.py
--- a/commitindex/commitindex/api/iterations.py
+++ b/commitindex/commitindex/api/iterations.py
@@ -1,20 +1,27 @@
 # 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/.
 from connexion import problem, request
+from flask import current_app
 
 from commitindex.reviews.triggers import get_bugzilla_client, trigger_review
+from commitindex.commits.mercurial import Mercurial
 
 
 def search():
     pass
 
 
+def get_mercurial_client():
+    """Return Mercurial client instance."""
+    return Mercurial(rest_url=current_app.config['HG_SERVER_URL'])
+
+
 def get(id):
     # TODO: Attempt to find a persisted iteration matching the request.
 
     # We could not find a matching iteration.
     return problem(
         404,
         'Iteration not found',
         'The requested iteration does not exist',
@@ -40,20 +47,60 @@ def post(data):
             401, 'Invalid Bugzilla header values',
             'The Bugzilla API headers in the request were not valid'
         )
 
     topic = data.get('topic', 1)
 
     # TODO: Topic lookup and validation
 
+    # Validate the commits with the mercurial server
+    data['commits'] = fetch_commit_data(data['commits'])
+
     # Trigger review creation for this iteration.
     trigger_review(data['commits'], request.header['X-Bugzilla-API-Key'])
 
     return {
         'data': {
             'id': 1,
             'topic': topic,
             'commits': [{
                 'id': commit
             } for commit in data['commits']],
         },
     }, 200
+
+
+def fetch_commit_data(commits, repo):
+    """
+    Take a list of commits ids, and fetch data for each from the
+    Mercurial server.
+
+    TODO:
+    1. host variable should go away and be pulled in from some
+    configuration file or environment variable. It is added here
+    to facilitate mountebank testing.
+    """
+
+    mercurial = get_mercurial_client()
+
+    commit_data = []
+
+    for commit in commits:
+        data = mercurial.get_commit_data(repo, commit)
+        commit_data.append(data)
+
+    return commit_data
+
+
+def fetch_commit_diff(commit, repo):
+    """
+    Fetch the full diff for a specified commit.
+
+    TODO:
+    1. host variable should go away and be pulled in from some
+    configuration file or environment variable. It is added here
+    to facilitate mountebank testing.
+    """
+
+    mercurial = get_mercurial_client()
+
+    return mercurial.get_commit_diff(repo, commit)
--- a/commitindex/commitindex/commits/mercurial.py
+++ b/commitindex/commitindex/commits/mercurial.py
@@ -9,24 +9,21 @@ import logging
 import requests
 
 logger = logging.getLogger(__name__)
 
 
 class Mercurial(object):
     """
     Interface to a Mercurial system.
-
-    TODO:
-    1. Load API URL from system wide config
     """
 
     def __init__(self, rest_url):
         self.rest_url = rest_url
-        self.session = requests.Session()
+        self.session  = requests.Session()
 
     def call(self, method, path, data=None, raw=False):
         """Perform API call and decode JSON.
 
         Generic call function that performs an API call to the
         Mercurial system and turns the JSON data returned into a
         Python data object.
 
@@ -56,29 +53,29 @@ class Mercurial(object):
                 self.rest_url + path, params=data, headers=headers
             )
 
         if method == 'POST':
             response = self.session.post(
                 self.rest_url + path, json=data, headers=headers
             )
 
+        if not response.ok:
+            raise MercurialError(response.reason, response.status_code)
+
         if not raw and \
            'Content-Type' in response.headers and \
            response.headers['Content-Type'] == 'application/json':
             try:
                 data = json.loads(response.content.decode())
             except:
                 raise MercurialError("Error decoding JSON data", 400)
         else:
             data = str(response.content.decode())
 
-        if isinstance(data, dict) and 'error' in data:
-            raise MercurialError(data['message'], data['code'])
-
         return data
 
     def get_commit_data(self, repo, commit):
         """Get meta data about the commit from Merurial.
 
         Params:
             repo: Path for the repo
             commit: Commit node for the data to retrieve.
@@ -86,57 +83,40 @@ class Mercurial(object):
         Returns:
             Python dict containing meta data about commit.
 
         Raises:
             MercurialError: General error where the fault code and string will
             pertain to the specific error code generated by Mercurial.
         """
 
-        try:
-            commit_data = self.call(
-                'GET',
-                '/' + quote(str(repo)) + '/json-rev/' + quote(str(commit))
-            )
-        except MercurialError as error:
-            raise MercurialError(
-                'Resource not found or Mercurial server not available.',
-                error.fault_code
-            )
-
-        return commit_data
+        return self.call(
+            'GET', '/' + quote(str(repo)) + '/json-rev/' + quote(str(commit))
+        )
 
     def get_commit_diff(self, repo, commit):
         """Get raw diff belonging the commit from Merurial.
 
         Params:
             repo: Path for the repo
             commit: Commit node for the data to retrieve.
 
         Returns:
             Raw diff (text)
 
         Raises:
             MercurialError: General error where the fault code and string will
             pertain to the specific error code generated by Mercurial.
         """
 
-        try:
-            diff = self.call(
-                'GET',
-                '/' + quote(str(repo)) + '/raw-rev/' + quote(str(commit)),
-                raw=True
-            )
-        except MercurialError as error:
-            raise MercurialError(
-                'Resource not found or Mercurial server not available.',
-                error.fault_code
-            )
-
-        return diff
+        return self.call(
+            'GET',
+            '/' + quote(str(repo)) + '/raw-rev/' + quote(str(commit)),
+            raw=True
+        )
 
 
 class MercurialError(Exception):
     """Generic Merurial Exception"""
 
     def __init__(self, msg, code=None):
         super(MercurialError, self).__init__(msg)
         self.fault_code = int(code)
--- a/commitindex/commitindex/reviews/bugzilla.py
+++ b/commitindex/commitindex/reviews/bugzilla.py
@@ -120,17 +120,20 @@ class Bugzilla(object):
             BugzillaError: General error where the fault code and string will
             pertain to the specific error code generated by Bugzilla.
         """
 
         params = {'login': quote(username)}
 
         try:
             self.call(
-                'GET', '/rest/valid_login', data=params, api_key=quote(api_key)
+                'GET',
+                '/rest/valid_login',
+                data=params,
+                api_key=quote(api_key)
             )
         except BugzillaError as error:
             if error.fault_code == 306:
                 return False
             raise
 
         return True
 
new file mode 100644
--- /dev/null
+++ b/commitindex/tests/test_hg_server.py
@@ -0,0 +1,281 @@
+# 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/.
+"""
+Mountebank test cases for commit-index
+"""
+
+from commitindex.commitindex import app
+from commitindex.api.iterations import (
+    get_mercurial_client, fetch_commit_data, fetch_commit_diff
+)
+from commitindex.commits.mercurial import MercurialError
+from testing import MountebankClient
+from flask import current_app
+import pytest
+
+COMMIT_DIFF = """diff --git a/dirs/source.py b/dirs/source.py
+--- a/dirs/source.py
++++ b/dirs/source.py
+@@ -1,8 +1,17 @@
+
++from commitindex.reviews.bugzilla import Bugzilla
+"""
+
+
+class FakeMercurial:
+    """Setups up the imposter test double emulating Mercurial"""
+
+    def __init__(self, mountebank_client):
+        self.mountebank = mountebank_client
+
+    @property
+    def url(self):
+        """Return fully qualified url for server"""
+
+        # Copied from the project docker-compose.yml file.
+        return 'http://mountebank:' + str(self.mountebank.imposter_port)
+
+    def create_commit_stubs(self, commit_data):
+        """Get detail information about specific revids from Mercurial."""
+
+        data_path = '/automation/conduit/json-rev/'
+        diff_path = '/automation/conduit/raw-rev/'
+
+        create_stubs = []
+        for commit_node in commit_data.keys():
+            create_stubs.append(
+                {
+                    "predicates": [
+                        {
+                            "equals": {
+                                "method": "GET",
+                                "path": data_path + commit_node
+                            }
+                        }
+                    ],
+                    "responses": [
+                        {
+                            "is": {
+                                "statusCode": 200,
+                                "headers": {
+                                    "Content-Type": "application/json"
+                                },
+                                "body": {
+                                    "node": commit_node,
+                                    "date": [1490110970.0, 14400],
+                                    "desc": "commit description",
+                                    "backedoutby": "",
+                                    "branch": "default",
+                                    "bookmarks": [],
+                                    "tags": [],
+                                    "user": "Joe Developer <joe@mexample.com>",
+                                    "parents": [commit_data[commit_node]],
+                                    "phase": "public",
+                                    "pushid": 75,
+                                    "pushdate": [1490146788, 0],
+                                    "pushuser": "joe@example.com"
+                                }
+                            }
+                        }
+                    ]
+                }
+            )
+            create_stubs.append(
+                {
+                    "predicates": [
+                        {
+                            "equals": {
+                                "method": "GET",
+                                "path": diff_path + commit_node
+                            }
+                        }
+                    ],
+                    "responses": [
+                        {
+                            "is": {
+                                "statusCode": 200,
+                                "headers": {
+                                    "Content-Type": "text/plain"
+                                },
+                                "body": COMMIT_DIFF
+                            }
+                        }
+                    ]
+                }
+            )
+
+        predicates_not_found = []
+        for commit_node in commit_data.keys():
+            predicates_not_found.append(
+                {
+                    "not": {
+                        "equals": {
+                            "path": data_path + commit_node
+                        }
+                    }
+                }
+            )
+            predicates_not_found.append(
+                {
+                    "not": {
+                        "equals": {
+                            "path": diff_path + commit_node
+                        }
+                    }
+                }
+            )
+
+        create_stubs.append(
+            {
+                "predicates": predicates_not_found,
+                "responses": [{
+                    "is": {
+                        "statusCode": 404
+                    }
+                }]
+            }
+        )
+        self.mountebank.create_stub(create_stubs)
+
+
+@pytest.fixture(scope='session')
+def mountebank():
+    """Returns configured Mounteback client instance"""
+
+    # 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 mercurial(request, mountebank):
+    """Returns emulated Mercurial service methods"""
+
+    # 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 FakeMercurial(mountebank)
+
+
+@pytest.fixture
+def fake_commit_data(mercurial):
+    """Setup stubs for testing."""
+
+    # commit_node : parent_node
+    commit_data = {
+        'f5279c5bcc6c74e9cf767fad36930e9ae7d09bdc':
+        '797fef1ce31a759dcea06e8cd269405bcbd14f96',
+        '48982f7f928b8c8a77433bf7b1fa986fda4b239d':
+        'f5279c5bcc6c74e9cf767fad36930e9ae7d09bdc'
+    }
+
+    mercurial.create_commit_stubs(commit_data)
+
+    return commit_data
+
+
+def test_mercurial_client_properly_created():
+    """Tests that a Mercurial client is properly created with URL"""
+
+    hg_server_url = 'http://blah/'
+    with app.app.app_context():
+        current_app.config['HG_SERVER_URL'] = hg_server_url
+
+        client = get_mercurial_client()
+        assert client.rest_url == hg_server_url
+
+
+def test_get_commit_data_success(fake_commit_data, mercurial):
+    """
+    Tests for successfully getting extended commit data from Mercurial.
+    """
+
+    with app.app.app_context():
+        current_app.config['HG_SERVER_URL'] = mercurial.url
+
+        results = fetch_commit_data(
+            [x for x in fake_commit_data.keys()],
+            'automation/conduit',
+        )
+
+        for result in results:
+            assert result['node'] in fake_commit_data.keys()
+
+
+def test_get_commit_data_bad_commit_raises_error(fake_commit_data, mercurial):
+    """
+    Tests for proper failure from Mercurial when using bad commit node.
+    """
+
+    with app.app.app_context():
+        current_app.config['HG_SERVER_URL'] = mercurial.url
+
+        with pytest.raises(MercurialError):
+            fetch_commit_data(['12345'], 'automation/conduit')
+
+
+def test_get_commit_data_bad_repo_raises_error(fake_commit_data, mercurial):
+    """
+    Tests for proper failure from Mercurial when using bad repo name.
+    """
+
+    with app.app.app_context():
+        current_app.config['HG_SERVER_URL'] = mercurial.url
+
+        with pytest.raises(MercurialError):
+            fetch_commit_data(
+                ['f5279c5bcc6c74e9cf767fad36930e9ae7d09bdc'],
+                'automation/bugzilla'
+            )
+
+
+def test_get_commit_diff_success(fake_commit_data, mercurial):
+    """
+    Tests for successfully getting raw diff from Mercurial.
+    """
+
+    with app.app.app_context():
+        current_app.config['HG_SERVER_URL'] = mercurial.url
+
+        result = fetch_commit_diff(
+            'f5279c5bcc6c74e9cf767fad36930e9ae7d09bdc', 'automation/conduit'
+        )
+        assert result == COMMIT_DIFF
+
+
+def test_get_commit_diff_bad_commit_raises_error(fake_commit_data, mercurial):
+    """
+    Tests for proper failure Mercurial when using bad commit node.
+    """
+
+    with app.app.app_context():
+        current_app.config['HG_SERVER_URL'] = mercurial.url
+
+        with pytest.raises(MercurialError):
+            fetch_commit_diff('12345', 'automation/conduit')
+
+
+def test_get_commit_diff_bad_repo_raises_error(fake_commit_data, mercurial):
+    """
+    Tests for proper failure from Mercurial when using bad repo.
+    """
+
+    with app.app.app_context():
+        current_app.config['HG_SERVER_URL'] = mercurial.url
+
+        with pytest.raises(MercurialError):
+            fetch_commit_diff(
+                'f5279c5bcc6c74e9cf767fad36930e9ae7d09bdc',
+                'automation/bugzilla'
+            )