Merge lp:~fwereade/pyjuju/webdav-auth-alternative into lp:pyjuju

Proposed by William Reade
Status: Merged
Approved by: Kapil Thangavelu
Approved revision: 342
Merged at revision: 408
Proposed branch: lp:~fwereade/pyjuju/webdav-auth-alternative
Merge into: lp:pyjuju
Diff against target: 1200 lines (+728/-211)
14 files modified
juju/environment/config.py (+4/-1)
juju/providers/orchestra/__init__.py (+2/-4)
juju/providers/orchestra/digestauth.py (+105/-0)
juju/providers/orchestra/files.py (+30/-23)
juju/providers/orchestra/tests/common.py (+36/-7)
juju/providers/orchestra/tests/data/server.crt (+12/-0)
juju/providers/orchestra/tests/data/server.key (+15/-0)
juju/providers/orchestra/tests/test_bootstrap.py (+3/-6)
juju/providers/orchestra/tests/test_connect.py (+3/-3)
juju/providers/orchestra/tests/test_digestauth.py (+302/-0)
juju/providers/orchestra/tests/test_files.py (+181/-122)
juju/providers/orchestra/tests/test_findzookeepers.py (+22/-26)
juju/providers/orchestra/tests/test_shutdown.py (+4/-9)
juju/providers/orchestra/tests/test_state.py (+9/-10)
To merge this branch: bzr merge lp:~fwereade/pyjuju/webdav-auth-alternative
Reviewer Review Type Date Requested Status
Benjamin Saller (community) Approve
Kapil Thangavelu (community) Approve
Review via email: mp+78836@code.launchpad.net

Description of the change

I think this is a slightly cleaner approach.

The big drawback of this branch (from a certain perspective) is that unrelated tests are mocking out get_page_auth (rather than setting up a local server and running the full interaction, as is done in tests for get_page_auth itself, and those for FileStorage which use it directly). IMO, mocking at this level is justifiable(-shading-to-desirable), simply because trying to run a server with (almost) every Orchestra test has a disproportionately obfuscating effect on those tests (changes to orchestra-server and/or storage-url in config, determined at runtime, plus additional complexity on every url check).

The big benefit is that authentication itself is much more cleanly separated from the rest of the code, and it will be much easier to fix the authentication in isolation if/when we need to deal with other mechanisms and/or implementations.

To post a comment you must log in.
338. By William Reade

merge trunk

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

much nicer, and full of awesome, +1

[0] has this been tested against a live apache install?

review: Approve
Revision history for this message
William Reade (fwereade) wrote :

[0]

Yep.

339. By William Reade

merge trunk

340. By William Reade

merge trunk

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

ne minor i noticed in a second pass.

[0]

66 + realm = info["realm"]
67 + nonce = info["nonce"]
68 + algorithm = info.get("algorithm", "MD5")
69 + if algorithm != "MD5":
70 + raise ProviderError("Unsupported digest algorithm: %s" % algorithm)
71 + qop = info["qop"]

Seems like all the info retrieval, should be behind a keyerror catch, broken
implementations abound in external systems.

341. By William Reade

hazmat's followup review point; imports in test_files

342. By William Reade

merge trunk

Revision history for this message
Benjamin Saller (bcsaller) wrote :

This branch looks good, thanks. +1

[1] trivial: wondering about the style of

+ d.addErrback(_convert_404, url)
+ d.addErrback(_convert_unknown_twisted_error, "GET", url)
+ d.addErrback(convert_unknown_error)

which seems to me a way of building a switch out of private functions where evolution here would require adding another function and more errbacks. Unless I misread wouldn't it be more maintainable to put the error handling switch in a single errback so that any changes only needed to occur in the errback and not in any of the places it registered as well (as they'd already have the orig function). get_page_auth already has the HTTP method which is the only part of the errorback chain thats somewhat modular and could return the defer with the errbacks already bound as expected.

Feel free to disagree, but this is a case where modularity only asks us to repeat ourselves.

review: Approve
Revision history for this message
William Reade (fwereade) wrote :

> Feel free to disagree, but this is a case where modularity only asks us to
> repeat ourselves.

I can see that _convert_unknown_twisted_error and convert_unknown_error are used (almost) identically in .get() and .put(), and it would make sense for _c_u_t_e to just call c_u_e itself. However, the important bits are the 404 and 401 handlers respectively, and they have to run before we fall back to the generic error handling; we could pass, say, a dict of codes to handlers, but that feels like a step back.

I don't think it's appropriate to put the 401 fallback handler in get_page_auth, simply because it then makes get_page_auth work on a different level to getPage; IMO, it's cognitively useful to have getPage (which it's reasonable to assume familiarity with) next to, and used the same as, the unfamiliar get_page_auth. It also fels slightly icky to handle 401s and 404s at different levels of the program.

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

pep8 trivial, need some spaces
+ self.quietLoss=True

343. By William Reade

address review points

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'juju/environment/config.py'
2--- juju/environment/config.py 2011-10-06 22:24:21 +0000
3+++ juju/environment/config.py 2011-10-17 07:46:17 +0000
4@@ -53,9 +53,12 @@
5 "acquired-mgmt-class": String(),
6 "available-mgmt-class": String(),
7 "storage-url": String(),
8+ "storage-user": String(),
9+ "storage-pass": String(),
10 "placement": String(),
11 "default-series": String()},
12- optional=["storage-url", "placement"]),
13+ optional=["storage-url", "storage-user",
14+ "storage-pass", "placement"]),
15 "local": KeyDict({"admin-secret": String(),
16 "data-dir": String(),
17 "placement": Constant("local"),
18
19=== modified file 'juju/providers/orchestra/__init__.py'
20--- juju/providers/orchestra/__init__.py 2011-09-23 20:35:02 +0000
21+++ juju/providers/orchestra/__init__.py 2011-10-17 07:46:17 +0000
22@@ -19,6 +19,7 @@
23 def __init__(self, environment_name, config):
24 super(MachineProvider, self).__init__(environment_name, config)
25 self.cobbler = CobblerClient(config)
26+ self._storage = FileStorage(config)
27
28 @property
29 def provider_type(self):
30@@ -26,10 +27,7 @@
31
32 def get_file_storage(self):
33 """Return a WebDAV-backed FileStorage abstraction."""
34- if "storage-url" not in self.config:
35- return FileStorage(
36- "http://%(orchestra-server)s/webdav" % self.config)
37- return FileStorage(self.config["storage-url"])
38+ return self._storage
39
40 def start_machine(self, machine_data, master=False):
41 """Start an Orchestra machine.
42
43=== added file 'juju/providers/orchestra/digestauth.py'
44--- juju/providers/orchestra/digestauth.py 1970-01-01 00:00:00 +0000
45+++ juju/providers/orchestra/digestauth.py 2011-10-17 07:46:17 +0000
46@@ -0,0 +1,105 @@
47+from hashlib import md5
48+from urllib2 import parse_http_list, parse_keqv_list
49+from uuid import uuid4
50+
51+from twisted.internet import reactor
52+from twisted.internet.ssl import ClientContextFactory
53+from twisted.python.failure import Failure
54+from twisted.web.client import HTTPClientFactory, HTTPPageGetter
55+from twisted.web.error import Error
56+
57+from juju.errors import ProviderError
58+
59+
60+def _parse_auth_info(auth_info):
61+ method, info_str = auth_info.split(' ', 1)
62+ if method != "Digest":
63+ raise ProviderError("Unknown authentication method: %s" % method)
64+ items = parse_http_list(info_str)
65+ info = parse_keqv_list(items)
66+
67+ try:
68+ qop = info["qop"]
69+ realm = info["realm"]
70+ nonce = info["nonce"]
71+ except KeyError as e:
72+ raise ProviderError(
73+ "Authentication request missing required key: %s" % e)
74+ algorithm = info.get("algorithm", "MD5")
75+ if algorithm != "MD5":
76+ raise ProviderError("Unsupported digest algorithm: %s" % algorithm)
77+ if "auth" not in qop.split(","):
78+ raise ProviderError("Unsupported quality-of-protection: %s" % qop)
79+ return realm, nonce, "auth", algorithm
80+
81+
82+def _digest_squish(*fields):
83+ return md5(":".join(fields)).hexdigest()
84+
85+
86+class DigestAuthenticator(object):
87+
88+ def __init__(self, username, password):
89+ self._user = username
90+ self._pass = password
91+ self._nonce_count = 0
92+
93+ def authenticate(self, method, url, auth_info):
94+ realm, nonce, qop, algorithm = _parse_auth_info(auth_info)
95+ ha1 = _digest_squish(self._user, realm, self._pass)
96+ ha2 = _digest_squish(method, url)
97+ cnonce = str(uuid4())
98+ self._nonce_count += 1
99+ nc = "%08x" % self._nonce_count
100+ response = _digest_squish(ha1, nonce, nc, cnonce, qop, ha2)
101+ return (
102+ 'Digest username="%s", realm="%s", nonce="%s", uri="%s", '
103+ 'algorithm="%s", response="%s", qop="%s", nc="%s", cnonce="%s"'
104+ % (self._user, realm, nonce, url, algorithm, response, qop,
105+ nc, cnonce))
106+
107+
108+def _connect(factory):
109+ if factory.scheme == 'https':
110+ reactor.connectSSL(
111+ factory.host, factory.port, factory, ClientContextFactory())
112+ else:
113+ reactor.connectTCP(factory.host, factory.port, factory)
114+
115+
116+class _AuthPageGetter(HTTPPageGetter):
117+
118+ handleStatus_204 = lambda self: self.handleStatus_200()
119+
120+ def handleStatus_401(self):
121+ if not self.factory.authenticated:
122+ (auth_info,) = self.headers["www-authenticate"]
123+ self.factory.authenticate(auth_info)
124+ _connect(self.factory)
125+ else:
126+ self.handleStatusDefault()
127+ self.factory.noPage(Failure(Error(self.status, self.message)))
128+ self.quietLoss = True
129+ self.transport.loseConnection()
130+
131+
132+class _AuthClientFactory(HTTPClientFactory):
133+
134+ protocol = _AuthPageGetter
135+ authenticated = False
136+
137+ def __init__(self, url, authenticator, **kwargs):
138+ HTTPClientFactory.__init__(self, url, **kwargs)
139+ self._authenticator = authenticator
140+
141+ def authenticate(self, auth_info):
142+ self.headers["authorization"] = self._authenticator.authenticate(
143+ self.method, self.url, auth_info)
144+ self.authenticated = True
145+
146+
147+def get_page_auth(url, authenticator, method="GET", postdata=None):
148+ factory = _AuthClientFactory(
149+ url, authenticator, method=method, postdata=postdata)
150+ _connect(factory)
151+ return factory.deferred
152
153=== modified file 'juju/providers/orchestra/files.py'
154--- juju/providers/orchestra/files.py 2011-09-15 18:50:23 +0000
155+++ juju/providers/orchestra/files.py 2011-10-17 07:46:17 +0000
156@@ -5,14 +5,32 @@
157 from twisted.web.client import getPage
158 from twisted.web.error import Error
159
160-from juju.errors import FileNotFound
161+from juju.errors import FileNotFound, ProviderError
162+from juju.providers.common.utils import convert_unknown_error
163+from juju.providers.orchestra.digestauth import (
164+ DigestAuthenticator, get_page_auth)
165+
166+
167+def _convert_error(failure, method, url, errors):
168+ if failure.check(Error):
169+ status = failure.value.status
170+ error = errors.get(int(status))
171+ if error:
172+ raise error
173+ raise ProviderError(
174+ "Unexpected HTTP %s trying to %s %s" % (status, method, url))
175+ return convert_unknown_error(failure)
176
177
178 class FileStorage(object):
179 """A WebDAV-backed :class:`FileStorage` abstraction"""
180
181- def __init__(self, base_url):
182- self._base_url = base_url
183+ def __init__(self, config):
184+ fallback_url = "http://%(orchestra-server)s/webdav" % config
185+ self._base_url = config.get("storage-url", fallback_url)
186+ self._auth = DigestAuthenticator(
187+ config.get("storage-user", config["orchestra-user"]),
188+ config.get("storage-pass", config["orchestra-pass"]))
189
190 def get_url(self, name):
191 """Return a URL that can be used to access a stored file.
192@@ -44,17 +62,11 @@
193 url = self.get_url(name)
194 d = getPage(url)
195 d.addCallback(StringIO)
196-
197- def convert_404(failure):
198- failure.trap(Error)
199- if failure.value.status == "404":
200- raise FileNotFound(url)
201- return failure
202- d.addErrback(convert_404)
203+ d.addErrback(_convert_error, "GET", url, {404: FileNotFound(url)})
204 return d
205
206- def put(self, remote_path, file_object):
207- """Upload a file to S3.
208+ def put(self, name, file_object):
209+ """Upload a file to WebDAV.
210
211 :param unicode remote_path: path on which to store the content
212
213@@ -62,16 +74,11 @@
214
215 :rtype: :class:`twisted.internet.defer.Deferred`
216 """
217- url = self.get_url(remote_path)
218- data = file_object.read()
219- d = getPage(url, method="PUT", postdata=data)
220+ url = self.get_url(name)
221+ postdata = file_object.read()
222+ d = get_page_auth(url, self._auth, method="PUT", postdata=postdata)
223 d.addCallback(lambda _: True)
224-
225- def accept_204(failure):
226- # NOTE: webdav returns 204s when we overwrite
227- failure.trap(Error)
228- if failure.value.status == "204":
229- return True
230- return failure
231- d.addErrback(accept_204)
232+ d.addErrback(_convert_error, "PUT", url, {401: ProviderError(
233+ "The supplied storage credentials were not accepted by the "
234+ "server")})
235 return d
236
237=== modified file 'juju/providers/orchestra/tests/common.py'
238--- juju/providers/orchestra/tests/common.py 2011-09-29 05:35:04 +0000
239+++ juju/providers/orchestra/tests/common.py 2011-10-17 07:46:17 +0000
240@@ -4,11 +4,11 @@
241 from xmlrpclib import Fault
242 from yaml import dump, load
243
244-from twisted.internet.defer import fail, succeed
245+from twisted.internet.defer import fail, succeed, inlineCallbacks, returnValue
246 from twisted.web.error import Error
247 from twisted.web.xmlrpc import Proxy
248
249-from juju.lib.mocker import MATCH
250+from juju.lib.mocker import ANY, MATCH
251 from juju.providers.orchestra import MachineProvider
252
253 DATA_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), "data")
254@@ -42,16 +42,45 @@
255 Proxy_m("http://somewhe.re/cobbler_api")
256 self.mocker.result(self.proxy_m)
257 self.getPage = self.mocker.replace("twisted.web.client.getPage")
258+ self.get_page_auth = self.mocker.replace(
259+ "juju.providers.orchestra.digestauth.get_page_auth")
260+
261+ def mock_fs_get(self, url, code, content=None):
262+ self.getPage(url)
263+ if code == 200:
264+ self.mocker.result(succeed(content))
265+ else:
266+ self.mocker.result(fail(Error(str(code))))
267+
268+ def mock_fs_put(self, url, expect, code=201):
269+ # NOTE: in some respects, it would be better to simulate the complete
270+ # interaction with the webdav provider; the factors that work against
271+ # doing so are:
272+ # 1) authentication is tested on DigestAuthenticator, on get_page_auth,
273+ # and again on FileStorage; even if it were easy to do so, testing
274+ # the same paths at yet another level starts to feel somewhat
275+ # superfluous.
276+ # 2) it's not *easy* to do so: we'd have a unique storage URL base for
277+ # every test, which involves a custom config dict for every test,
278+ # and unique URLs to check for every test that hits webdav, and
279+ # another base class to setUp/tearDown. that's not to say it's
280+ # *hard* to do so, but it's time-consuming and costs more complexity
281+ # -- in a large number of the orchestra tests -- than is warranted
282+ # by whatever additional verification it might allow for.
283+ self.get_page_auth(url, ANY, method="PUT", postdata=expect)
284+ if code in (201, 204):
285+ self.mocker.result(succeed(""))
286+ else:
287+ self.mocker.result(fail(Error(str(code))))
288
289 def mock_find_zookeepers(self, existing=None):
290- self.getPage("http://somewhe.re/webdav/provider-state")
291-
292+ url = "http://somewhe.re/webdav/provider-state"
293 if existing is None:
294- self.mocker.result(fail(Error("404")))
295+ self.mock_fs_get(url, 404)
296 else:
297 uid, name = existing
298- self.mocker.result(succeed(dump(
299- {"zookeeper-instances": [uid]})))
300+ content = dump({"zookeeper-instances": [uid]})
301+ self.mock_fs_get(url, 200, content)
302 self.mock_describe_systems(succeed([{
303 "uid": uid, "name": name, "mgmt_classes": ["acquired"]}]))
304
305
306=== added file 'juju/providers/orchestra/tests/data/server.crt'
307--- juju/providers/orchestra/tests/data/server.crt 1970-01-01 00:00:00 +0000
308+++ juju/providers/orchestra/tests/data/server.crt 2011-10-17 07:46:17 +0000
309@@ -0,0 +1,12 @@
310+-----BEGIN CERTIFICATE-----
311+MIIBtzCCASACCQCBVxLrMqKj9TANBgkqhkiG9w0BAQUFADAfMR0wGwYDVQQDExRl
312+bnNlbWJsZSB0ZXN0IHNlcnZlcjAgFw0xMTA5MDcxMzIwMDBaGA8yMTExMDgxNDEz
313+MjAwMFowHzEdMBsGA1UEAxMUZW5zZW1ibGUgdGVzdCBzZXJ2ZXIwgZ8wDQYJKoZI
314+hvcNAQEBBQADgY0AMIGJAoGBANUSvEXDA8HTbFVGi+v4akPHjOHicp8nYZK2m6wJ
315+R/jV0ACha6s10YMnT0pA+6Dpqms+isw51nP7tbnnRFvZL4qbLKTgO6Y2/mehb9k0
316+368Tesw0+xKU9ORBX78hlSU6YCfvxX6VxRDWocczCnTWuw2SDPf88kLFQTfzULEA
317+6+fRAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEARrXUZSdVsljB5lj2T1IqXQosPR+6
318+ndMAS9w05Qe4Jmo5wKrJwH7zwgki/ByS3yqTk9ADPoqZy0MU9WG1io3Z/3MFkg8Y
319+ywTdzm1FxJNWD9tTPlwQFR+BlBxpVLszPadwaZnPkYZ4a+WR5R1bohlGElvFZX9I
320+2ipfQu0TKxTdGuc=
321+-----END CERTIFICATE-----
322
323=== added file 'juju/providers/orchestra/tests/data/server.key'
324--- juju/providers/orchestra/tests/data/server.key 1970-01-01 00:00:00 +0000
325+++ juju/providers/orchestra/tests/data/server.key 2011-10-17 07:46:17 +0000
326@@ -0,0 +1,15 @@
327+-----BEGIN RSA PRIVATE KEY-----
328+MIICWwIBAAKBgQDVErxFwwPB02xVRovr+GpDx4zh4nKfJ2GStpusCUf41dAAoWur
329+NdGDJ09KQPug6aprPorMOdZz+7W550Rb2S+Kmyyk4DumNv5noW/ZNN+vE3rMNPsS
330+lPTkQV+/IZUlOmAn78V+lcUQ1qHHMwp01rsNkgz3/PJCxUE381CxAOvn0QIDAQAB
331+AoGAWMHpM5Y85mzP3+X3O2DLw1hI03+lB6878gWna06icIGAmAKl+zf8AopJeUEA
332+kNNFbk8rOk+Nidr8pGg2DZy3NF678Vy03x8lycoU6ndI6XTev4j4sZa7IMnDThrv
333+ldpXZsJfHL9CJXLSqFKZu1OFHxBZwIE794Yeh42QfxV99cECQQD7aE/ZYK583x/7
334+6rauUWU15xNNnOaGpqRNE2T3ZnU67xHlMCWa365MCYX6o2bNGi102LUsPxkgkm33
335+G7KAiQFNAkEA2Pcn+OGIjLpCSLJw18TbfzO7iDR5tIoec0QQXP3OP+iBQ24CPJW/
336+EUBqlMSyd00ORigP//T78AT4KHekT5O+lQJAaSXdj5siH1PquqAWO54LaJn2ttVS
337+jSqROTNNXTPbAAURRPv4HmhDK8Yn5QYGbu3t6Rrh21mglsDngRxycdPbWQJALMx7
338+uGv5IfWjkhcmLac8Gzu3URxktN6AAxTevBS77X44ko+4boIM/abrWuRyZSfH9rx2
339+8UbIbnrYMqLhjnzXMQJAacGuIeKxpiuwC9eJ6Iz+Vmh6bAbhrlfQgiPR2XZIEv8z
340+FqEl7L3i6K8l8tuLQFRtQuu1L9YGWvV+g7pDMaR/Bw==
341+-----END RSA PRIVATE KEY-----
342
343=== modified file 'juju/providers/orchestra/tests/test_bootstrap.py'
344--- juju/providers/orchestra/tests/test_bootstrap.py 2011-09-30 04:52:20 +0000
345+++ juju/providers/orchestra/tests/test_bootstrap.py 2011-10-17 07:46:17 +0000
346@@ -12,15 +12,12 @@
347 class OrchestraBootstrapTest(TestCase, OrchestraTestMixin):
348
349 def mock_verify(self):
350- self.getPage("http://somewhe.re/webdav/bootstrap-verify",
351- method="PUT", postdata="storage is writable")
352- self.mocker.result(succeed(True))
353+ self.mock_fs_put("http://somewhe.re/webdav/bootstrap-verify",
354+ "storage is writable")
355
356 def mock_save_state(self):
357 data = dump({"zookeeper-instances": ["winston-uid"]})
358- self.getPage("http://somewhe.re/webdav/provider-state",
359- method="PUT", postdata=data)
360- self.mocker.result(succeed(None))
361+ self.mock_fs_put("http://somewhe.re/webdav/provider-state", data)
362
363 def test_already_bootstrapped(self):
364 self.setup_mocks()
365
366=== modified file 'juju/providers/orchestra/tests/test_connect.py'
367--- juju/providers/orchestra/tests/test_connect.py 2011-09-15 18:50:23 +0000
368+++ juju/providers/orchestra/tests/test_connect.py 2011-10-17 07:46:17 +0000
369@@ -20,9 +20,9 @@
370
371 def mock_connect(self, share, result):
372 self.setup_mocks()
373- self.getPage("http://somewhe.re/webdav/provider-state")
374- self.mocker.result(succeed(dump(
375- {"zookeeper-instances": ["foo"]})))
376+ self.mock_fs_get(
377+ "http://somewhe.re/webdav/provider-state", 200, dump(
378+ {"zookeeper-instances": ["foo"]}))
379 self.proxy_m.callRemote("get_systems")
380 self.mocker.result(succeed([{"uid": "foo",
381 "name": "foo.example.com",
382
383=== added file 'juju/providers/orchestra/tests/test_digestauth.py'
384--- juju/providers/orchestra/tests/test_digestauth.py 1970-01-01 00:00:00 +0000
385+++ juju/providers/orchestra/tests/test_digestauth.py 2011-10-17 07:46:17 +0000
386@@ -0,0 +1,302 @@
387+import os
388+
389+from twisted.internet import reactor
390+from twisted.internet.defer import inlineCallbacks
391+from twisted.internet.ssl import DefaultOpenSSLContextFactory
392+from twisted.protocols.policies import WrappingFactory
393+from twisted.web.error import Error
394+from twisted.web.resource import Resource
395+from twisted.web.server import Site
396+
397+from juju.errors import ProviderError
398+from juju.lib.testing import TestCase
399+from juju.providers.orchestra.digestauth import (
400+ DigestAuthenticator, get_page_auth)
401+
402+
403+def data_file(name):
404+ return os.path.join(os.path.dirname(__file__), "data", name)
405+
406+
407+class DigestAuthenticatorTest(TestCase):
408+
409+ def setUp(self):
410+ super(DigestAuthenticatorTest, self).setUp()
411+ self.uuid4_m = self.mocker.replace("uuid.uuid4")
412+ self.auth = DigestAuthenticator("jiminy", "cricket")
413+
414+ def challenge(self, method="Digest", **kwargs):
415+ kwargs.setdefault("qop", "auth")
416+ details = ", ".join("%s=%s" % item for item in kwargs.items())
417+ return ("%s realm=loathing, nonce=blah, %s" % (method, details))
418+
419+ def default_response(self):
420+ return ('Digest username="jiminy", realm="loathing", nonce="blah", '
421+ 'uri="http://somewhe.re/", algorithm="MD5", '
422+ 'response="8891952040a96ba62cce585972bd3360", qop="auth", '
423+ 'nc="00000001", cnonce="twiddle"')
424+
425+ def assert_response(self, challenge, response):
426+ self.assertEquals(
427+ self.auth.authenticate("METH", "http://somewhe.re/", challenge),
428+ response)
429+
430+ def assert_error(self, challenge, err_type, err_message):
431+ error = self.assertRaises(
432+ err_type,
433+ self.auth.authenticate, "METH", "http://somewhe.re/", challenge)
434+ self.assertEquals(str(error), err_message)
435+
436+ def assert_missing_key(self, challenge, key):
437+ self.mocker.replay()
438+ message = "Authentication request missing required key: %r" % key
439+ self.assert_error(challenge, ProviderError, message)
440+
441+ def test_normal(self):
442+ self.uuid4_m()
443+ self.mocker.result("twiddle")
444+ self.mocker.replay()
445+ self.assert_response(self.challenge(), self.default_response())
446+
447+ def test_nc_increments(self):
448+ self.uuid4_m()
449+ self.mocker.result("twiddle")
450+ self.uuid4_m()
451+ self.mocker.result("twaddle")
452+ self.mocker.replay()
453+ self.assert_response(self.challenge(), self.default_response())
454+ self.assert_response(
455+ self.challenge(),
456+ 'Digest username="jiminy", realm="loathing", nonce="blah", '
457+ 'uri="http://somewhe.re/", algorithm="MD5", '
458+ 'response="c12e9ec2e0569d896b2f26b91c0e74c3", qop="auth", '
459+ 'nc="00000002", cnonce="twaddle"')
460+
461+ def test_qop_choices(self):
462+ self.uuid4_m()
463+ self.mocker.result("twiddle")
464+ self.mocker.replay()
465+ self.assert_response(self.challenge(qop='"auth,auth-int"'),
466+ self.default_response())
467+
468+ def test_specify_algorithm(self):
469+ self.uuid4_m()
470+ self.mocker.result("twiddle")
471+ self.mocker.replay()
472+ self.assert_response(self.challenge(algorithm="MD5"),
473+ self.default_response())
474+
475+ def test_bad_method(self):
476+ self.mocker.replay()
477+ self.assert_error(self.challenge("Masticate"), ProviderError,
478+ "Unknown authentication method: Masticate")
479+
480+ def test_bad_algorithm(self):
481+ self.mocker.replay()
482+ self.assert_error(self.challenge(algorithm="ROT13"), ProviderError,
483+ "Unsupported digest algorithm: ROT13")
484+
485+ def test_bad_qop(self):
486+ self.mocker.replay()
487+ self.assert_error(self.challenge(qop="auth-int"), ProviderError,
488+ "Unsupported quality-of-protection: auth-int")
489+
490+ def test_missing_keys(self):
491+ self.assert_missing_key("Digest realm=x, nonce=y", "qop")
492+ self.assert_missing_key("Digest realm=x, qop=y", "nonce")
493+ self.assert_missing_key("Digest qop=x, nonce=y", "realm")
494+
495+class PlainResource(Resource):
496+
497+ def __init__(self, test, method, content, expect_content=None, status=200):
498+ Resource.__init__(self)
499+ self._test = test
500+ self._method = method
501+ self._content = content
502+ self._expect_content = expect_content
503+ self._status = status
504+
505+ def render(self, request):
506+ self._test.assertEquals(request.method, self._method)
507+ if self._expect_content:
508+ self._test.assertEquals(
509+ request.content.read(), self._expect_content)
510+ request.setResponseCode(self._status)
511+ return self._content
512+
513+
514+class AuthResource(Resource):
515+
516+ def __init__(self, test, method, content, challenge, check_response,
517+ expect_content=None, status=200):
518+ Resource.__init__(self)
519+ self._test = test
520+ self._method = method
521+ self._content = content
522+ self._challenge = challenge
523+ self._check_response = check_response
524+ self._expect_content = expect_content
525+ self._status = status
526+ self._rendered = False
527+
528+ def render(self, request):
529+ if self._expect_content:
530+ self._test.assertEquals(
531+ request.content.read(), self._expect_content)
532+ if not self._rendered:
533+ self._rendered = True
534+ request.setResponseCode(401)
535+ request.setHeader("www-authenticate", self._challenge)
536+ return ""
537+ else:
538+ self._check_response(request.getHeader("authorization"))
539+ request.setResponseCode(self._status)
540+ return self._content
541+
542+
543+class GetPageAuthTestCase(TestCase):
544+
545+ scheme = "http"
546+
547+ def _listen(self, site):
548+ if self.scheme == "http":
549+ return reactor.listenTCP(0, site, interface="127.0.0.1")
550+ elif self.scheme == "https":
551+ sslFactory = DefaultOpenSSLContextFactory(
552+ data_file("server.key"), data_file("server.crt"))
553+ return reactor.listenSSL(
554+ 0, site, sslFactory, interface="127.0.0.1")
555+ else:
556+ self.fail("unknown scheme: %s" % self.scheme)
557+
558+ def setUp(self):
559+ super(GetPageAuthTestCase, self).setUp()
560+ self.root = Resource()
561+ self.wrapper = WrappingFactory(Site(self.root, timeout=None))
562+ self.port = self._listen(self.wrapper)
563+ self.portno = self.port.getHost().port
564+
565+ def tearDown(self):
566+ super(GetPageAuthTestCase, self).tearDown()
567+ if self.wrapper.protocols.keys():
568+ print "CONNECTION(S) ALIVE! buggy test?"
569+ return self.port.stopListening()
570+
571+ def get_url(self, path):
572+ return "%s/%s" % (self.get_base_url(), path)
573+
574+ def get_base_url(self):
575+ return "%s://127.0.0.1:%s" % (self.scheme, self.portno)
576+
577+ def add_plain(self, path, method, content,
578+ expect_content=None, status=200):
579+ self.root.putChild(path, PlainResource(
580+ self, method, content,
581+ expect_content=expect_content, status=status))
582+
583+ def add_auth(self, path, method, content, challenge, check_response,
584+ expect_content=None, status=200):
585+ self.root.putChild(path, AuthResource(
586+ self, method, content, challenge, check_response,
587+ expect_content=expect_content, status=status))
588+
589+
590+class GetPageAuthTestsMixin(object):
591+
592+ def setup_mock(self):
593+ self.uuid4_m = self.mocker.replace("uuid.uuid4")
594+
595+ @inlineCallbacks
596+ def test_404(self):
597+ # verify that usual mechanism is in place
598+ d = get_page_auth(self.get_url("missing"), None)
599+ error = yield self.assertFailure(d, Error)
600+ self.assertEquals(error.status, "404")
601+
602+ def test_default_method(self):
603+ self.add_plain("blah", "GET", "cheese")
604+ d = get_page_auth(self.get_url("blah"), None)
605+ d.addCallback(self.assertEquals, "cheese")
606+ return d
607+
608+ def test_other_method(self):
609+ self.add_plain("blob", "TWIDDLE", "pickle")
610+ d = get_page_auth(self.get_url("blob"), None, method="TWIDDLE")
611+ d.addCallback(self.assertEquals, "pickle")
612+ return d
613+
614+ def test_authenticate(self):
615+ self.setup_mock()
616+ self.uuid4_m()
617+ self.mocker.result("baguette")
618+ self.mocker.replay()
619+
620+ url = self.get_url("blip")
621+ auth = DigestAuthenticator("sandwich", "earl")
622+
623+ def check(response):
624+ self.assertTrue(response.startswith(
625+ 'Digest username="sandwich", realm="x", nonce="y", uri="%s"'
626+ % url))
627+ self.assertIn(
628+ 'qop="auth", nc="00000001", cnonce="baguette"', response)
629+
630+ self.add_auth(
631+ "blip", "PROD", "ham", "Digest realm=x, nonce=y, qop=auth", check)
632+ d = get_page_auth(url, auth, method="PROD")
633+ d.addCallback(self.assertEquals, "ham")
634+ return d
635+
636+ def test_authenticate_postdata_201(self):
637+ self.setup_mock()
638+ self.uuid4_m()
639+ self.mocker.result("ciabatta")
640+ self.mocker.replay()
641+
642+ url = self.get_url("blam")
643+ auth = DigestAuthenticator("pizza", "principessa")
644+
645+ def check(response):
646+ self.assertTrue(response.startswith(
647+ 'Digest username="pizza", realm="a", nonce="b", uri="%s"'
648+ % url))
649+ self.assertIn(
650+ 'qop="auth", nc="00000001", cnonce="ciabatta"', response)
651+
652+ self.add_auth(
653+ "blam", "FLING", "tomato", "Digest realm=a, nonce=b, qop=auth",
654+ check, expect_content="oven", status=201)
655+ d = get_page_auth(url, auth, method="FLING", postdata="oven")
656+ d.addCallback(self.assertEquals, "tomato")
657+ return d
658+
659+ def test_authenticate_postdata_204(self):
660+ self.setup_mock()
661+ self.uuid4_m()
662+ self.mocker.result("focaccia")
663+ self.mocker.replay()
664+
665+ url = self.get_url("blur")
666+ auth = DigestAuthenticator("quesadilla", "king")
667+
668+ def check(response):
669+ self.assertTrue(response.startswith(
670+ 'Digest username="quesadilla", realm="p", nonce="q", uri="%s"'
671+ % url))
672+ self.assertIn(
673+ 'qop="auth", nc="00000001", cnonce="focaccia"', response)
674+
675+ self.add_auth(
676+ "blur", "HURL", "", "Digest realm=p, nonce=q, qop=auth", check,
677+ expect_content="bbq", status=204)
678+ d = get_page_auth(url, auth, method="HURL", postdata="bbq")
679+ d.addCallback(self.assertEquals, "")
680+ return d
681+
682+
683+class GetPageAuthHttpTest(GetPageAuthTestCase, GetPageAuthTestsMixin):
684+ scheme = "http"
685+
686+
687+class GetPageAuthHttpsTest(GetPageAuthTestCase, GetPageAuthTestsMixin):
688+ scheme = "https"
689
690=== modified file 'juju/providers/orchestra/tests/test_files.py'
691--- juju/providers/orchestra/tests/test_files.py 2011-09-15 18:50:23 +0000
692+++ juju/providers/orchestra/tests/test_files.py 2011-10-17 07:46:17 +0000
693@@ -3,144 +3,203 @@
694 from twisted.internet.defer import fail, succeed
695 from twisted.web.error import Error
696
697-from juju.errors import FileNotFound
698+from juju.errors import FileNotFound, ProviderError, ProviderInteractionError
699 from juju.lib.testing import TestCase
700 from juju.providers.orchestra import MachineProvider
701
702+from .test_digestauth import GetPageAuthTestCase
703+
704+
705+class SomeError(Exception):
706+ pass
707+
708
709 def get_file_storage(custom_config=None):
710 config = {"orchestra-server": "somewhereel.se",
711- "orchestra-user": "user",
712- "orchestra-pass": "pass",
713+ "orchestra-user": "fallback-user",
714+ "orchestra-pass": "fallback-pass",
715 "acquired-mgmt-class": "acquired",
716 "available-mgmt-class": "available"}
717 if custom_config is None:
718- config["storage-url"] = "http://somewhe.re/webdav"
719+ config["storage-url"] = "http://somewhe.re"
720+ config["storage-user"] = "user"
721+ config["storage-pass"] = "pass"
722 else:
723 config.update(custom_config)
724 provider = MachineProvider("blah", config)
725 return provider.get_file_storage()
726
727
728-class FileStorageTest(TestCase):
729-
730- def test_get_works_no_storage_url(self):
731- getPage = self.mocker.replace("twisted.web.client.getPage")
732- getPage("http://somewhereel.se/webdav/rubber/chicken")
733- self.mocker.result(succeed("pulley"))
734- self.mocker.replay()
735-
736- fs = get_file_storage({})
737- d = fs.get("rubber/chicken")
738-
739- def verify(result):
740- self.assertEquals(result.read(), "pulley")
741- d.addCallback(verify)
742- return d
743-
744- def test_get_works(self):
745- getPage = self.mocker.replace("twisted.web.client.getPage")
746- getPage("http://somewhe.re/webdav/rubber/chicken")
747- self.mocker.result(succeed("pulley"))
748- self.mocker.replay()
749-
750- fs = get_file_storage()
751- d = fs.get("rubber/chicken")
752-
753- def verify(result):
754- self.assertEquals(result.read(), "pulley")
755- d.addCallback(verify)
756- return d
757-
758- def test_get_fails(self):
759- getPage = self.mocker.replace("twisted.web.client.getPage")
760- getPage("http://somewhe.re/webdav/rubber/chicken")
761- self.mocker.result(fail(Error("404")))
762- self.mocker.replay()
763-
764- fs = get_file_storage()
765- d = fs.get("rubber/chicken")
766- self.assertFailure(d, FileNotFound)
767- return d
768-
769- def test_get_errors(self):
770- getPage = self.mocker.replace("twisted.web.client.getPage")
771- getPage("http://somewhe.re/webdav/rubber/chicken")
772- self.mocker.result(fail(Error("500")))
773- self.mocker.replay()
774-
775- fs = get_file_storage()
776- d = fs.get("rubber/chicken")
777- self.assertFailure(d, Error)
778- return d
779+class FileStorageGetTest(TestCase):
780+
781+ def setUp(self):
782+ self.uuid4_m = self.mocker.replace("uuid.uuid4")
783+ self.getPage = self.mocker.replace("twisted.web.client.getPage")
784
785 def test_get_url(self):
786- fs = get_file_storage()
787- url = fs.get_url("rubber/chicken")
788- self.assertEqual(url, "http://somewhe.re/webdav/rubber/chicken")
789-
790- def test_get_url_unicode(self):
791- fs = get_file_storage({"storage-url": u"http://\u2666.co.\u2660"})
792- url = fs.get_url(u"rubber/\u2665/chicken")
793- self.assertEqual(
794- url, "http://xn--h6h.co.xn--b6h/rubber/%E2%99%A5/chicken")
795- self.assertInstance(url, str)
796-
797- def test_put_works(self):
798- getPage = self.mocker.replace("twisted.web.client.getPage")
799- getPage("http://somewhe.re/webdav/rubber/chicken",
800- method="PUT", postdata="pulley")
801- self.mocker.result(succeed(None))
802- self.mocker.replay()
803-
804- fs = get_file_storage()
805- d = fs.put("rubber/chicken", StringIO("pulley"))
806-
807- def verify(result):
808- self.assertEquals(result, True)
809- d.addCallback(verify)
810- return d
811-
812- def test_put_works_no_storage_url(self):
813- getPage = self.mocker.replace("twisted.web.client.getPage")
814- getPage("http://somewhereel.se/webdav/rubber/chicken",
815- method="PUT", postdata="pulley")
816- self.mocker.result(succeed(None))
817- self.mocker.replay()
818-
819+ self.mocker.replay()
820+ fs = get_file_storage()
821+ self.assertEquals(fs.get_url("angry/birds"),
822+ "http://somewhe.re/angry/birds")
823+
824+ def test_get_url_fallback(self):
825+ self.mocker.replay()
826 fs = get_file_storage({})
827- d = fs.put("rubber/chicken", StringIO("pulley"))
828-
829- def verify(result):
830- self.assertEquals(result, True)
831- d.addCallback(verify)
832- return d
833-
834- def test_put_handles_204(self):
835- """If we're overwriting instead of creating, we get 204 instead of 200
836- """
837- getPage = self.mocker.replace("twisted.web.client.getPage")
838- getPage("http://somewhe.re/webdav/rubber/chicken",
839- method="PUT", postdata="pulley")
840- self.mocker.result(fail(Error("204")))
841- self.mocker.replay()
842-
843- fs = get_file_storage()
844- d = fs.put("rubber/chicken", StringIO("pulley"))
845-
846- def verify(result):
847- self.assertEquals(result, True)
848- d.addCallback(verify)
849- return d
850-
851- def test_put_errors(self):
852- getPage = self.mocker.replace("twisted.web.client.getPage")
853- getPage("http://somewhe.re/webdav/rubber/chicken",
854- method="PUT", postdata="pulley")
855- self.mocker.result(fail(Error("500")))
856- self.mocker.replay()
857-
858- fs = get_file_storage()
859- d = fs.put("rubber/chicken", StringIO("pulley"))
860- self.assertFailure(d, Error)
861+ self.assertEquals(fs.get_url("angry/birds"),
862+ "http://somewhereel.se/webdav/angry/birds")
863+
864+ def test_get(self):
865+ self.getPage("http://somewhe.re/rubber/chicken")
866+ self.mocker.result(succeed("pulley"))
867+ self.mocker.replay()
868+
869+ fs = get_file_storage()
870+ d = fs.get("rubber/chicken")
871+
872+ def verify(result):
873+ self.assertEquals(result.read(), "pulley")
874+ d.addCallback(verify)
875+ return d
876+
877+ def check_get_error(self, result, err_type, err_message):
878+ self.getPage("http://somewhe.re/rubber/chicken")
879+ self.mocker.result(result)
880+ self.mocker.replay()
881+
882+ fs = get_file_storage()
883+ d = fs.get("rubber/chicken")
884+ self.assertFailure(d, err_type)
885+
886+ def verify(error):
887+ self.assertEquals(str(error), err_message)
888+ d.addCallback(verify)
889+ return d
890+
891+ def test_get_error(self):
892+ return self.check_get_error(
893+ fail(SomeError("pow!")),
894+ ProviderInteractionError,
895+ "Unexpected SomeError interacting with provider: pow!")
896+
897+ def test_get_404(self):
898+ return self.check_get_error(
899+ fail(Error("404")),
900+ FileNotFound,
901+ "File was not found: 'http://somewhe.re/rubber/chicken'")
902+
903+ def test_get_bad_code(self):
904+ return self.check_get_error(
905+ fail(Error("999")),
906+ ProviderError,
907+ "Unexpected HTTP 999 trying to GET "
908+ "http://somewhe.re/rubber/chicken")
909+
910+
911+class FileStoragePutTest(GetPageAuthTestCase):
912+
913+ def setup_mock(self):
914+ self.uuid4_m = self.mocker.replace("uuid.uuid4")
915+
916+ def get_file_storage(self, with_user=True):
917+ storage_url = self.get_base_url()
918+ custom_config = {"storage-url": storage_url}
919+ if with_user:
920+ custom_config["storage-user"] = "user"
921+ custom_config["storage-pass"] = "pass"
922+ return get_file_storage(custom_config)
923+
924+ def test_no_auth_error(self):
925+ self.add_plain("peregrine", "PUT", "", "croissant", 999)
926+ fs = self.get_file_storage()
927+ d = fs.put("peregrine", StringIO("croissant"))
928+ self.assertFailure(d, ProviderError)
929+
930+ def verify(error):
931+ self.assertIn("Unexpected HTTP 999 trying to PUT ", str(error))
932+ d.addCallback(verify)
933+ return d
934+
935+ def test_no_auth_201(self):
936+ self.add_plain("peregrine", "PUT", "", "croissant", 201)
937+ fs = self.get_file_storage()
938+ d = fs.put("peregrine", StringIO("croissant"))
939+ d.addCallback(self.assertEquals, True)
940+ return d
941+
942+ def test_no_auth_204(self):
943+ self.add_plain("peregrine", "PUT", "", "croissant", 204)
944+ fs = self.get_file_storage()
945+ d = fs.put("peregrine", StringIO("croissant"))
946+ d.addCallback(self.assertEquals, True)
947+ return d
948+
949+ def auth_common(self, username, status, with_user=True):
950+ self.setup_mock()
951+ self.uuid4_m()
952+ self.mocker.result("dinner")
953+ self.mocker.replay()
954+
955+ url = self.get_url("possum")
956+
957+ def check(response):
958+ self.assertTrue(response.startswith(
959+ 'Digest username="%s", realm="sparta", nonce="meh", uri="%s"'
960+ % (username, url)))
961+ self.assertIn(
962+ 'qop="auth", nc="00000001", cnonce="dinner"', response)
963+ self.add_auth(
964+ "possum", "PUT", "", "Digest realm=sparta, nonce=meh, qop=auth",
965+ check, expect_content="canabalt", status=status)
966+
967+ fs = self.get_file_storage(with_user)
968+ return fs.put("possum", StringIO("canabalt"))
969+
970+ def test_auth_error(self):
971+ d = self.auth_common("user", 808)
972+ self.assertFailure(d, ProviderError)
973+
974+ def verify(error):
975+ self.assertIn("Unexpected HTTP 808 trying to PUT", str(error))
976+ d.addCallback(verify)
977+ return d
978+
979+ def test_auth_bad_credentials(self):
980+ d = self.auth_common("user", 401)
981+ self.assertFailure(d, ProviderError)
982+
983+ def verify(error):
984+ self.assertEquals(
985+ str(error),
986+ "The supplied storage credentials were not accepted by the "
987+ "server")
988+ d.addCallback(verify)
989+ return d
990+
991+ def test_auth_201(self):
992+ d = self.auth_common("user", 201)
993+ d.addCallback(self.assertEquals, True)
994+ return d
995+
996+ def test_auth_204(self):
997+ d = self.auth_common("user", 204)
998+ d.addCallback(self.assertEquals, True)
999+ return d
1000+
1001+ def test_auth_fallback_error(self):
1002+ d = self.auth_common("fallback-user", 747, False)
1003+ self.assertFailure(d, ProviderError)
1004+
1005+ def verify(error):
1006+ self.assertIn("Unexpected HTTP 747 trying to PUT", str(error))
1007+ d.addCallback(verify)
1008+ return d
1009+
1010+ def test_auth_fallback_201(self):
1011+ d = self.auth_common("fallback-user", 201, False)
1012+ d.addCallback(self.assertEquals, True)
1013+ return d
1014+
1015+ def test_auth_fallback_204(self):
1016+ d = self.auth_common("fallback-user", 204, False)
1017+ d.addCallback(self.assertEquals, True)
1018 return d
1019
1020=== modified file 'juju/providers/orchestra/tests/test_findzookeepers.py'
1021--- juju/providers/orchestra/tests/test_findzookeepers.py 2011-09-15 18:50:23 +0000
1022+++ juju/providers/orchestra/tests/test_findzookeepers.py 2011-10-17 07:46:17 +0000
1023@@ -1,14 +1,14 @@
1024 from yaml import dump
1025
1026-from twisted.internet.defer import fail, succeed
1027-from twisted.web.error import Error
1028-from twisted.web.xmlrpc import Proxy
1029+from twisted.internet.defer import succeed
1030
1031 from juju.errors import EnvironmentNotFound
1032 from juju.lib.testing import TestCase
1033 from juju.providers.orchestra import MachineProvider
1034 from juju.providers.orchestra.machine import OrchestraMachine
1035
1036+from .common import OrchestraTestMixin
1037+
1038 CONFIG = {"orchestra-server": "somewhe.re",
1039 "storage-url": "http://somewhe.re/webdav",
1040 "orchestra-user": "user",
1041@@ -17,15 +17,14 @@
1042 "available-mgmt-class": "available"}
1043
1044
1045-class FindZookeepersTest(TestCase):
1046+class FindZookeepersTest(TestCase, OrchestraTestMixin):
1047
1048 def get_provider(self):
1049 return MachineProvider("tetrascape", CONFIG)
1050
1051- def mock_load_state(self, result):
1052- getPage = self.mocker.replace("twisted.web.client.getPage")
1053- getPage("http://somewhe.re/webdav/provider-state")
1054- self.mocker.result(result)
1055+ def mock_load_state(self, code, content):
1056+ self.mock_fs_get(
1057+ "http://somewhe.re/webdav/provider-state", code, content)
1058
1059 def assert_no_environment(self):
1060 provider = self.get_provider()
1061@@ -33,41 +32,38 @@
1062 self.assertFailure(d, EnvironmentNotFound)
1063 return d
1064
1065- def verify_no_environment(self, load_result):
1066- self.mock_load_state(load_result)
1067+ def verify_no_environment(self, code, content):
1068+ self.mock_load_state(code, content)
1069 self.mocker.replay()
1070 return self.assert_no_environment()
1071
1072 def test_no_state(self):
1073- self.verify_no_environment(fail(Error("404")))
1074+ self.setup_mocks()
1075+ self.verify_no_environment(404, None)
1076
1077 def test_empty_state(self):
1078- self.verify_no_environment(succeed(dump([])))
1079+ self.setup_mocks()
1080+ self.verify_no_environment(200, dump([]))
1081
1082 def test_no_hosts(self):
1083- self.verify_no_environment(succeed(dump({"abc": 123})))
1084+ self.setup_mocks()
1085+ self.verify_no_environment(200, dump({"abc": 123}))
1086
1087 def test_bad_instance(self):
1088- self.mock_load_state(succeed(dump({"zookeeper-instances": ["foo"]})))
1089- proxy_m = self.mocker.mock(Proxy)
1090- Proxy_m = self.mocker.replace(Proxy, spec=None)
1091- Proxy_m("http://somewhe.re/cobbler_api")
1092- self.mocker.result(proxy_m)
1093- proxy_m.callRemote("get_systems")
1094+ self.setup_mocks()
1095+ self.mock_load_state(200, dump({"zookeeper-instances": ["foo"]}))
1096+ self.proxy_m.callRemote("get_systems")
1097 self.mocker.result(succeed([]))
1098 self.mocker.replay()
1099
1100 return self.assert_no_environment()
1101
1102 def test_eventual_success(self):
1103- self.mock_load_state(succeed(dump({
1104- "zookeeper-instances": ["bad", "foo", "missing", "bar"]})))
1105- proxy_m = self.mocker.mock(Proxy)
1106- Proxy_m = self.mocker.replace(Proxy, spec=None)
1107- Proxy_m("http://somewhe.re/cobbler_api")
1108- self.mocker.result(proxy_m)
1109+ self.setup_mocks()
1110+ self.mock_load_state(200, dump({
1111+ "zookeeper-instances": ["bad", "foo", "missing", "bar"]}))
1112 for _ in range(4):
1113- proxy_m.callRemote("get_systems")
1114+ self.proxy_m.callRemote("get_systems")
1115 self.mocker.result(succeed([
1116 {"uid": "bad", "mgmt_classes": ["whatever"]},
1117 {"uid": "foo", "mgmt_classes": ["acquired"], "name": "foo"},
1118
1119=== modified file 'juju/providers/orchestra/tests/test_shutdown.py'
1120--- juju/providers/orchestra/tests/test_shutdown.py 2011-09-15 18:50:23 +0000
1121+++ juju/providers/orchestra/tests/test_shutdown.py 2011-10-17 07:46:17 +0000
1122@@ -169,16 +169,12 @@
1123
1124 def test_destroy_environment(self):
1125 self.setup_mocks()
1126- self.getPage("http://somewhe.re/webdav/provider-state",
1127- method="PUT", postdata="{}\n")
1128- self.mocker.result(succeed(True))
1129+ self.mock_fs_put("http://somewhe.re/webdav/provider-state", "{}\n")
1130 return self.check_shutdown_all("destroy_environment")
1131
1132 def test_destroy_environment_no_machines(self):
1133 self.setup_mocks()
1134- self.getPage("http://somewhe.re/webdav/provider-state",
1135- method="PUT", postdata="{}\n")
1136- self.mocker.result(succeed(True))
1137+ self.mock_fs_put("http://somewhe.re/webdav/provider-state", "{}\n")
1138 self.mock_get_systems()
1139 self.mocker.replay()
1140
1141@@ -189,7 +185,6 @@
1142
1143 def test_destroy_environment_unwritable(self):
1144 self.setup_mocks()
1145- self.getPage("http://somewhe.re/webdav/provider-state",
1146- method="PUT", postdata="{}\n")
1147- self.mocker.result(fail(SomeError()))
1148+ self.mock_fs_put(
1149+ "http://somewhe.re/webdav/provider-state", "{}\n", 500)
1150 return self.check_shutdown_all("destroy_environment")
1151
1152=== modified file 'juju/providers/orchestra/tests/test_state.py'
1153--- juju/providers/orchestra/tests/test_state.py 2011-09-15 18:50:23 +0000
1154+++ juju/providers/orchestra/tests/test_state.py 2011-10-17 07:46:17 +0000
1155@@ -1,10 +1,10 @@
1156 from yaml import dump
1157
1158-from twisted.internet.defer import succeed
1159-
1160 from juju.lib.testing import TestCase
1161 from juju.providers.orchestra import MachineProvider
1162
1163+from .common import OrchestraTestMixin
1164+
1165
1166 def get_provider():
1167 config = {"orchestra-server": "somewhe.re",
1168@@ -15,14 +15,13 @@
1169 return MachineProvider("tetrascape", config)
1170
1171
1172-class StateTest(TestCase):
1173+class StateTest(TestCase, OrchestraTestMixin):
1174
1175 def test_save(self):
1176+ self.setup_mocks()
1177 state = {"foo": "blah blah"}
1178- getPage = self.mocker.replace("twisted.web.client.getPage")
1179- getPage("http://somewhe.re/webdav/provider-state",
1180- method="PUT", postdata=dump(state))
1181- self.mocker.result(succeed(None))
1182+ self.mock_fs_put(
1183+ "http://somewhe.re/webdav/provider-state", dump(state))
1184 self.mocker.replay()
1185
1186 provider = get_provider()
1187@@ -34,10 +33,10 @@
1188 return d
1189
1190 def test_load(self):
1191+ self.setup_mocks()
1192 expect_state = {"foo": "blah blah"}
1193- getPage = self.mocker.replace("twisted.web.client.getPage")
1194- getPage("http://somewhe.re/webdav/provider-state")
1195- self.mocker.result(succeed(dump(expect_state)))
1196+ self.mock_fs_get(
1197+ "http://somewhe.re/webdav/provider-state", 200, dump(expect_state))
1198 self.mocker.replay()
1199
1200 provider = get_provider()

Subscribers

People subscribed via source and target branches

to status/vote changes: