Merge ~cjwatson/turnip:hookrpc-test into turnip:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: b545f1b6d7e00fab5d76905187130fb1ce803652
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/turnip:hookrpc-test
Merge into: turnip:master
Diff against target: 313 lines (+175/-76)
3 files modified
turnip/pack/tests/fake_servers.py (+91/-0)
turnip/pack/tests/test_functional.py (+5/-74)
turnip/pack/tests/test_hookrpc.py (+79/-2)
Reviewer Review Type Date Requested Status
Tom Wardill (community) Approve
Launchpad code reviewers Pending
Review via email: mp+359119@code.launchpad.net

Commit message

Add unit tests for HookRPCHandler

Description of the change

It's tested by way of functional tests, but unit tests are easier to deal with in some cases.

To post a comment you must log in.
Revision history for this message
Tom Wardill (twom) :
review: Approve
Revision history for this message
Otto Co-Pilot (otto-copilot) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/turnip/pack/tests/fake_servers.py b/turnip/pack/tests/fake_servers.py
2new file mode 100644
3index 0000000..1e0f310
4--- /dev/null
5+++ b/turnip/pack/tests/fake_servers.py
6@@ -0,0 +1,91 @@
7+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
8+# GNU Affero General Public License version 3 (see the file LICENSE).
9+
10+from __future__ import (
11+ absolute_import,
12+ print_function,
13+ unicode_literals,
14+ )
15+
16+from collections import defaultdict
17+import hashlib
18+
19+from lazr.sshserver.auth import NoSuchPersonWithName
20+from twisted.web import xmlrpc
21+
22+__all__ = [
23+ "FakeAuthServerService",
24+ "FakeVirtInfoService",
25+ ]
26+
27+
28+class FakeAuthServerService(xmlrpc.XMLRPC):
29+ """A fake version of the Launchpad authserver service."""
30+
31+ def __init__(self):
32+ xmlrpc.XMLRPC.__init__(self)
33+ self.keys = defaultdict(list)
34+
35+ def addSSHKey(self, username, public_key_path):
36+ with open(public_key_path, "r") as f:
37+ public_key = f.read()
38+ kind, keytext, _ = public_key.split(" ", 2)
39+ if kind == "ssh-rsa":
40+ keytype = "RSA"
41+ elif kind == "ssh-dss":
42+ keytype = "DSA"
43+ else:
44+ raise Exception("Unrecognised public key type %s" % kind)
45+ self.keys[username].append((keytype, keytext))
46+
47+ def xmlrpc_getUserAndSSHKeys(self, username):
48+ if username not in self.keys:
49+ raise NoSuchPersonWithName(username)
50+ return {
51+ "id": hash(username) % (2 ** 31),
52+ "name": username,
53+ "keys": self.keys[username],
54+ }
55+
56+
57+class FakeVirtInfoService(xmlrpc.XMLRPC):
58+ """A trivial virt information XML-RPC service.
59+
60+ Translates a path to its SHA-256 hash. The repo is writable if the
61+ path is prefixed with '/+rw'
62+ """
63+
64+ def __init__(self, *args, **kwargs):
65+ xmlrpc.XMLRPC.__init__(self, *args, **kwargs)
66+ self.require_auth = False
67+ self.translations = []
68+ self.authentications = []
69+ self.push_notifications = []
70+ self.ref_permissions_checks = []
71+ self.ref_permissions = {}
72+ self.ref_permissions_as_dict = False
73+
74+ def xmlrpc_translatePath(self, pathname, permission, auth_params):
75+ if self.require_auth and 'user' not in auth_params:
76+ raise xmlrpc.Fault(3, "Unauthorized")
77+
78+ self.translations.append((pathname, permission, auth_params))
79+ writable = False
80+ if pathname.startswith('/+rw'):
81+ writable = True
82+ pathname = pathname[4:]
83+
84+ if permission != b'read' and not writable:
85+ raise xmlrpc.Fault(2, "Repository is read-only")
86+ return {'path': hashlib.sha256(pathname).hexdigest()}
87+
88+ def xmlrpc_authenticateWithPassword(self, username, password):
89+ self.authentications.append((username, password))
90+ return {'user': username}
91+
92+ def xmlrpc_notify(self, path):
93+ self.push_notifications.append(path)
94+
95+ def xmlrpc_checkRefPermissions(self, path, ref_paths, auth_params):
96+ self.ref_permissions_checks.append((path, ref_paths, auth_params))
97+ return self.ref_permissions
98diff --git a/turnip/pack/tests/test_functional.py b/turnip/pack/tests/test_functional.py
99index 142de34..2fa1782 100644
100--- a/turnip/pack/tests/test_functional.py
101+++ b/turnip/pack/tests/test_functional.py
102@@ -1,5 +1,5 @@
103 # -*- coding: utf-8 -*-
104-# Copyright 2015 Canonical Ltd. This software is licensed under the
105+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
106 # GNU Affero General Public License version 3 (see the file LICENSE).
107
108 from __future__ import (
109@@ -9,7 +9,6 @@ from __future__ import (
110 )
111
112 import base64
113-from collections import defaultdict
114 import hashlib
115 import io
116 import os
117@@ -32,7 +31,6 @@ from fixtures import (
118 EnvironmentVariable,
119 TempDir,
120 )
121-from lazr.sshserver.auth import NoSuchPersonWithName
122 from pygit2 import GIT_OID_HEX_ZERO
123 from testtools import TestCase
124 from testtools.content import text_content
125@@ -62,80 +60,13 @@ from turnip.pack.hookrpc import (
126 )
127 from turnip.pack.http import SmartHTTPFrontendResource
128 from turnip.pack.ssh import SmartSSHService
129+from turnip.pack.tests.fake_servers import (
130+ FakeAuthServerService,
131+ FakeVirtInfoService,
132+ )
133 from turnip.version_info import version_info
134
135
136-class FakeAuthServerService(xmlrpc.XMLRPC):
137- """A fake version of the Launchpad authserver service."""
138-
139- def __init__(self):
140- xmlrpc.XMLRPC.__init__(self)
141- self.keys = defaultdict(list)
142-
143- def addSSHKey(self, username, public_key_path):
144- with open(public_key_path, "r") as f:
145- public_key = f.read()
146- kind, keytext, _ = public_key.split(" ", 2)
147- if kind == "ssh-rsa":
148- keytype = "RSA"
149- elif kind == "ssh-dss":
150- keytype = "DSA"
151- else:
152- raise Exception("Unrecognised public key type %s" % kind)
153- self.keys[username].append((keytype, keytext))
154-
155- def xmlrpc_getUserAndSSHKeys(self, username):
156- if username not in self.keys:
157- raise NoSuchPersonWithName(username)
158- return {
159- "id": hash(username) % (2 ** 31),
160- "name": username,
161- "keys": self.keys[username],
162- }
163-
164-
165-class FakeVirtInfoService(xmlrpc.XMLRPC):
166- """A trivial virt information XML-RPC service.
167-
168- Translates a path to its SHA-256 hash. The repo is writable if the
169- path is prefixed with '/+rw'
170- """
171-
172- def __init__(self, *args, **kwargs):
173- xmlrpc.XMLRPC.__init__(self, *args, **kwargs)
174- self.require_auth = False
175- self.translations = []
176- self.authentications = []
177- self.push_notifications = []
178- self.ref_permissions_checks = []
179- self.ref_permissions = {}
180-
181- def xmlrpc_translatePath(self, pathname, permission, auth_params):
182- if self.require_auth and 'user' not in auth_params:
183- raise xmlrpc.Fault(3, "Unauthorized")
184-
185- self.translations.append((pathname, permission, auth_params))
186- writable = False
187- if pathname.startswith('/+rw'):
188- writable = True
189- pathname = pathname[4:]
190-
191- if permission != b'read' and not writable:
192- raise xmlrpc.Fault(2, "Repository is read-only")
193- return {'path': hashlib.sha256(pathname).hexdigest()}
194-
195- def xmlrpc_authenticateWithPassword(self, username, password):
196- self.authentications.append((username, password))
197- return {'user': username}
198-
199- def xmlrpc_notify(self, path):
200- self.push_notifications.append(path)
201-
202- def xmlrpc_checkRefPermissions(self, path, ref_paths, auth_params):
203- self.ref_permissions_checks.append((path, ref_paths, auth_params))
204- return self.ref_permissions
205-
206-
207 class FunctionalTestMixin(object):
208
209 run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=30)
210diff --git a/turnip/pack/tests/test_hookrpc.py b/turnip/pack/tests/test_hookrpc.py
211index aca320a..9af0408 100644
212--- a/turnip/pack/tests/test_hookrpc.py
213+++ b/turnip/pack/tests/test_hookrpc.py
214@@ -1,4 +1,4 @@
215-# Copyright 2015 Canonical Ltd. This software is licensed under the
216+# Copyright 2015-2018 Canonical Ltd. This software is licensed under the
217 # GNU Affero General Public License version 3 (see the file LICENSE).
218
219 from __future__ import (
220@@ -7,11 +7,20 @@ from __future__ import (
221 unicode_literals,
222 )
223
224+import contextlib
225+import uuid
226+
227 from testtools import TestCase
228-from twisted.internet import defer
229+from testtools.deferredruntest import AsynchronousDeferredRunTest
230+from twisted.internet import (
231+ defer,
232+ reactor,
233+ )
234 from twisted.test import proto_helpers
235+from twisted.web import server
236
237 from turnip.pack import hookrpc
238+from turnip.pack.tests.fake_servers import FakeVirtInfoService
239
240
241 class DummyJSONNetstringProtocol(hookrpc.JSONNetstringProtocol):
242@@ -127,3 +136,71 @@ class TestRPCServerProtocol(TestCase):
243 self.assertEqual(
244 b'42:{"error": "Command must be a JSON object"},',
245 self.transport.value())
246+
247+
248+class TestHookRPCHandler(TestCase):
249+ """Test the hook RPC handler."""
250+
251+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
252+
253+ def setUp(self):
254+ super(TestHookRPCHandler, self).setUp()
255+ self.virtinfo = FakeVirtInfoService(allowNone=True)
256+ self.virtinfo_listener = reactor.listenTCP(
257+ 0, server.Site(self.virtinfo))
258+ self.virtinfo_port = self.virtinfo_listener.getHost().port
259+ self.virtinfo_url = b'http://localhost:%d/' % self.virtinfo_port
260+ self.addCleanup(self.virtinfo_listener.stopListening)
261+ self.hookrpc_handler = hookrpc.HookRPCHandler(self.virtinfo_url)
262+
263+ @contextlib.contextmanager
264+ def registeredKey(self, path, auth_params=None, permissions=None):
265+ key = str(uuid.uuid4())
266+ self.hookrpc_handler.registerKey(key, path, auth_params or {})
267+ if permissions is not None:
268+ self.hookrpc_handler.ref_permissions[key] = permissions
269+ try:
270+ yield key
271+ finally:
272+ self.hookrpc_handler.unregisterKey(key)
273+
274+ def assertCheckedRefPermissions(self, path, ref_paths, auth_params):
275+ self.assertEqual(
276+ [(path, ref_paths, auth_params)],
277+ self.virtinfo.ref_permissions_checks)
278+
279+ @defer.inlineCallbacks
280+ def test_checkRefPermissions_fresh(self):
281+ self.virtinfo.ref_permissions = {
282+ 'refs/heads/master': ['push'],
283+ 'refs/heads/next': ['force_push'],
284+ }
285+ with self.registeredKey('/translated', auth_params={'uid': 42}) as key:
286+ permissions = yield self.hookrpc_handler.checkRefPermissions(
287+ None,
288+ {'key': key, 'paths': sorted(self.virtinfo.ref_permissions)})
289+ self.assertEqual(self.virtinfo.ref_permissions, permissions)
290+ self.assertCheckedRefPermissions(
291+ '/translated', [b'refs/heads/master', b'refs/heads/next'],
292+ {'uid': 42})
293+
294+ @defer.inlineCallbacks
295+ def test_checkRefPermissions_cached(self):
296+ cached_ref_permissions = {
297+ 'refs/heads/master': ['push'],
298+ 'refs/heads/next': ['force_push'],
299+ }
300+ with self.registeredKey(
301+ '/translated', auth_params={'uid': 42},
302+ permissions=cached_ref_permissions) as key:
303+ permissions = yield self.hookrpc_handler.checkRefPermissions(
304+ None, {'key': key, 'paths': ['refs/heads/master']})
305+ expected_permissions = {'refs/heads/master': ['push']}
306+ self.assertEqual(expected_permissions, permissions)
307+ self.assertEqual([], self.virtinfo.ref_permissions_checks)
308+
309+ @defer.inlineCallbacks
310+ def test_notifyPush(self):
311+ with self.registeredKey('/translated') as key:
312+ yield self.hookrpc_handler.notifyPush(None, {'key': key})
313+ self.assertEqual(['/translated'], self.virtinfo.push_notifications)

Subscribers

People subscribed via source and target branches