Merge lp:~fwereade/pyjuju/webdav-auth-alternative into lp:pyjuju
- webdav-auth-alternative
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Benjamin Saller (community) | Approve | ||
Kapil Thangavelu (community) | Approve | ||
Review via email: mp+78836@code.launchpad.net |
Commit message
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(
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.
- 338. By William Reade
-
merge trunk
William Reade (fwereade) wrote : | # |
[0]
Yep.
- 339. By William Reade
-
merge trunk
- 340. By William Reade
-
merge trunk
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(
69 + if algorithm != "MD5":
70 + raise ProviderError(
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
Benjamin Saller (bcsaller) wrote : | # |
This branch looks good, thanks. +1
[1] trivial: wondering about the style of
+ d.addErrback(
+ d.addErrback(
+ d.addErrback(
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.
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_
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.
Kapil Thangavelu (hazmat) wrote : | # |
pep8 trivial, need some spaces
+ self.quietLoss=True
- 343. By William Reade
-
address review points
Preview Diff
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() |
much nicer, and full of awesome, +1
[0] has this been tested against a live apache install?