← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] lp:~thomir/launchpad/devel-gpgservice-fixture into lp:launchpad

 

Thomi Richards has proposed merging lp:~thomir/launchpad/devel-gpgservice-fixture into lp:launchpad with lp:~thomir/launchpad/devel-add-gpgservice-to-buildout as a prerequisite.

Commit message:
Add GPGKeyServiceFixture for testing integration against gpgservice.

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~thomir/launchpad/devel-gpgservice-fixture/+merge/286101

Add a fixture that knows how to run the gpgservice instance, and set it up with test data.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~thomir/launchpad/devel-gpgservice-fixture into lp:launchpad.
=== modified file 'lib/lp/services/config/schema-lazr.conf'
--- lib/lp/services/config/schema-lazr.conf	2015-12-02 11:27:02 +0000
+++ lib/lp/services/config/schema-lazr.conf	2016-02-16 02:08:45 +0000
@@ -781,6 +781,11 @@
 # datatype: string
 maps_api_key:
 
+[gpg_service]
+# The ip_address:port we can contact the gpgservice http api.
+# datatype: string
+service_location:
+
 [gpghandler]
 # Should we allow uploading keys to the keyserver?
 # datatype: boolean

=== added directory 'lib/lp/testing/gpgservice'
=== added file 'lib/lp/testing/gpgservice/__init__.py'
--- lib/lp/testing/gpgservice/__init__.py	1970-01-01 00:00:00 +0000
+++ lib/lp/testing/gpgservice/__init__.py	2016-02-16 02:08:45 +0000
@@ -0,0 +1,10 @@
+# Copyright 2015 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from __future__ import absolute_import
+
+from . _fixture import GPGKeyServiceFixture
+
+__all__ = [
+    'GPGKeyServiceFixture'
+    ]

=== added file 'lib/lp/testing/gpgservice/_fixture.py'
--- lib/lp/testing/gpgservice/_fixture.py	1970-01-01 00:00:00 +0000
+++ lib/lp/testing/gpgservice/_fixture.py	2016-02-16 02:08:45 +0000
@@ -0,0 +1,160 @@
+# Copyright 2015 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from __future__ import absolute_import
+
+from ConfigParser import SafeConfigParser
+import httplib
+import json
+import os.path
+import socket
+from StringIO import StringIO
+import subprocess
+from tempfile import NamedTemporaryFile
+from textwrap import dedent
+import time
+
+from fixtures import Fixture
+
+from lp.testing.keyserver import KeyServerTac
+from lp.services.config import config
+
+__metaclass__ = type
+
+
+class GPGKeyServiceFixture(Fixture):
+
+    """Run the gpgservice webapp and test key server."""
+
+    def __init__(self, config_fixture=None):
+        """Create a new GPGKeyServiceFixture.
+
+        :param config_fixture: If specified, this must be a ConfigFixture
+            instance. It will be updated with the relevant GPG service config
+            details.
+        """
+        self._config_fixture = config_fixture
+
+    def setUp(self):
+        super(GPGKeyServiceFixture, self).setUp()
+        # Figure out if the keyserver is running,and if not, run it:
+        keyserver = KeyServerTac()
+        if not os.path.exists(keyserver.pidfile):
+            self.useFixture(KeyServerTac())
+
+        # Write service config to a file on disk. This file gets deleted when the
+        # fixture ends.
+        service_config = _get_default_service_config()
+        self._config_file = NamedTemporaryFile()
+        self.addCleanup(self._config_file.close)
+        service_config.write(self._config_file)
+        self._config_file.flush()
+
+        # Set the environment variable that tells gpgservice where to read it's
+        # config file from:
+        env = os.environ.copy()
+        env['GPGSERVICE_CONFIG_PATH'] = self._config_file.name
+
+        gunicorn_path = os.path.join(
+            config.root, 'bin', 'gunicorn-for-gpgservice')
+        self.interface = '127.0.0.1'
+        self.port = _get_unused_port()
+        gunicorn_options = ['-b', self.bind_address]
+        wsgi_app_name = 'gpgservice.webapp:app'
+
+        self._process = subprocess.Popen(
+            args=[gunicorn_path] + gunicorn_options + [wsgi_app_name],
+            stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE,
+            env=env)
+        self.addCleanup(self._kill_server)
+        self._wait_for_service_start()
+        self.reset_service_database()
+        if self._config_fixture is not None:
+            config_section = service_config = dedent("""\
+                [gpg_service]
+                service_location: %s
+                """ % self.bind_address)
+            self._config_fixture.add_section(config_section)
+            config.reloadConfig()
+
+    def _kill_server(self):
+        self._process.terminate()
+        stdout, stderr = self._process.communicate()
+        self.addDetail('gunicorn-stdout', stdout)
+        self.addDetail('gunicorn-stderr', stderr)
+
+    def _wait_for_service_start(self):
+        errors = []
+        for i in range(10):
+            conn = httplib.HTTPConnection(self.bind_address)
+            try:
+                conn.request('GET', '/')
+            except socket.error as e:
+                errors.append(e)
+            else:
+                resp = conn.getresponse()
+                if resp.status == 200:
+                    return
+            time.sleep(0.1)
+        raise RuntimeError("Service not responding: %r" % errors)
+
+    def reset_service_database(self):
+        """Reset the gpgservice instance database to the launchpad test data set."""
+        conn = httplib.HTTPConnection(self.bind_address)
+        test_data = {
+            'keys': [
+                {
+                    'owner': 'name16_oid',
+                    'id': '12345678',
+                    'fingerprint': 'ABCDEF0123456789ABCDDCBA0000111112345678',
+                    'size': 1024,
+                    'algorithm': 'D',
+                    'can_encrypt': True,
+                    'enabled': True,
+                }
+            ]
+        }
+        headers = {'Content-Type': 'application/json'}
+        conn.request('POST', '/test/reset_db', json.dumps(test_data), headers)
+        resp = conn.getresponse()
+        body = resp.read()
+        if resp.status != 200:
+            raise RuntimeError("Could not reset database: %s" % body)
+
+    @property
+    def bind_address(self):
+        return '%s:%d' % (self.interface, self.port)
+
+
+def _get_default_service_config():
+    config = SafeConfigParser()
+    config.readfp(StringIO(dedent("""\
+        [gpghandler]
+        host: localhost
+        public_host: keyserver.ubuntu.com
+        upload_keys: True
+        port: 11371
+        timeout: 5.0
+        maximum_upload_size: 16777216
+        enable_test_endpoint: true
+
+        [database]
+        type: sqlite
+    """)))
+    return config
+
+
+def _get_unused_port():
+    """Find and return an unused port
+
+    There is a small race condition here (between the time we allocate the
+    port, and the time it actually gets used), but for the purposes for which
+    this function gets used it isn't a problem in practice.
+    """
+    s = socket.socket()
+    try:
+        s.bind(('localhost', 0))
+        return s.getsockname()[1]
+    finally:
+        s.close()

=== added directory 'lib/lp/testing/gpgservice/tests'
=== added file 'lib/lp/testing/gpgservice/tests/__init__.py'
=== added file 'lib/lp/testing/gpgservice/tests/test_fixture.py'
--- lib/lp/testing/gpgservice/tests/test_fixture.py	1970-01-01 00:00:00 +0000
+++ lib/lp/testing/gpgservice/tests/test_fixture.py	2016-02-16 02:08:45 +0000
@@ -0,0 +1,63 @@
+# Copyright 2015 Canonical Ltd.  This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+from __future__ import absolute_import
+
+import json
+import httplib
+
+from testtools import TestCase
+from testtools.matchers import (
+    Contains,
+    HasLength,
+    Not,
+    PathExists,
+)
+
+from lp.services.config import config
+from lp.services.config.fixture import (
+    ConfigFixture,
+    ConfigUseFixture,
+    )
+from lp.testing.layers import BaseLayer
+from lp.testing.gpgservice import GPGKeyServiceFixture
+
+
+class GPGServiceFixtureTests(TestCase):
+
+    layer = BaseLayer
+
+    def test_fixture_writes_and_deletes_service_config_file(self):
+        fixture = GPGKeyServiceFixture()
+        with fixture:
+            config_file_path = fixture._config_file.name
+            self.assertThat(config_file_path, PathExists())
+        self.assertThat(config_file_path, Not(PathExists()))
+
+    def test_fixture_starts_gpgservice(self):
+        fixture = self.useFixture(GPGKeyServiceFixture())
+        conn = httplib.HTTPConnection(fixture.bind_address)
+        conn.request('GET', '/')
+        resp = conn.getresponse()
+        self.assertEqual(200, resp.status)
+        self.assertEqual('gpgservice - Copyright 2015 Canonical', resp.read())
+
+    def test_fixture_can_create_test_data(self):
+        fixture = self.useFixture(GPGKeyServiceFixture())
+        conn = httplib.HTTPConnection(fixture.bind_address)
+        conn.request('GET', '/users/name16_oid/keys')
+        resp = conn.getresponse()
+        self.assertEqual(200, resp.status)
+        data = json.loads(resp.read())
+        self.assertThat(data, Contains('keys'))
+        self.assertThat(data['keys'], HasLength(1))
+
+    def test_fixture_can_set_config_data(self):
+        config_name = self.getUniqueString()
+        config_fixture = self.useFixture(
+            ConfigFixture(config_name, BaseLayer.config_fixture.instance_name))
+        self.useFixture(ConfigUseFixture(config_name))
+        gpg_fixture = self.useFixture(GPGKeyServiceFixture(config_fixture))
+
+        self.assertEqual(
+            gpg_fixture.bind_address, config.gpg_service.service_location)


Follow ups