Merge lp:~rharding/charmworld/proof-config-2 into lp:~juju-jitsu/charmworld/trunk
- proof-config-2
- Merge into trunk
Proposed by
Richard Harding
Status: | Merged |
---|---|
Merged at revision: | 426 |
Proposed branch: | lp:~rharding/charmworld/proof-config-2 |
Merge into: | lp:~juju-jitsu/charmworld/trunk |
Diff against target: |
1009 lines (+419/-407) 9 files modified
charmworld/lib/proof.py (+0/-33) charmworld/lib/tests/test_proof.py (+2/-72) charmworld/models.py (+17/-1) charmworld/tests/test_models.py (+39/-0) charmworld/views/api/__init__.py (+4/-132) charmworld/views/api/proof.py (+145/-0) charmworld/views/tests/test_api.py (+4/-169) charmworld/views/tests/test_proof.py (+175/-0) charmworld/views/utils.py (+33/-0) |
To merge this branch: | bzr merge lp:~rharding/charmworld/proof-config-2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju-Jitsu Hackers | Pending | ||
Review via email: mp+192239@code.launchpad.net |
Commit message
Refactor the proof api code to make more usable.
Description of the change
Refactor the proof api code to make more usable.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'charmworld/lib/proof.py' |
2 | --- charmworld/lib/proof.py 2013-10-18 18:32:48 +0000 |
3 | +++ charmworld/lib/proof.py 2013-10-22 20:55:07 +0000 |
4 | @@ -1,10 +1,4 @@ |
5 | """Helpers to proof charms and bundles.""" |
6 | -import yaml |
7 | - |
8 | -from charmworld.models import ( |
9 | - BundledCharmDescription, |
10 | - resolve_charm_from_description, |
11 | -) |
12 | |
13 | |
14 | class ProofError(Exception): |
15 | @@ -14,33 +8,6 @@ |
16 | self.msg = msg |
17 | |
18 | |
19 | -class BundleProof(object): |
20 | - |
21 | - @staticmethod |
22 | - def check_parseable_deployer_file(data, format='yaml'): |
23 | - try: |
24 | - bundle_data = yaml.safe_load(data) |
25 | - except yaml.YAMLError: |
26 | - raise ProofError( |
27 | - data, |
28 | - 'Could not parse the yaml provided.' |
29 | - ) |
30 | - return bundle_data |
31 | - |
32 | - @staticmethod |
33 | - def check_service_exists(db, name, service_data, bundle_config): |
34 | - charm_description = BundledCharmDescription( |
35 | - 'proof', service_data, bundle_config.get('series')) |
36 | - charm_found = resolve_charm_from_description(db, charm_description) |
37 | - if not charm_found: |
38 | - raise ProofError( |
39 | - charm_description.get_process(), |
40 | - 'Could not find charm: ' + name, |
41 | - ) |
42 | - else: |
43 | - return charm_found |
44 | - |
45 | - |
46 | class CharmProof(object): |
47 | |
48 | @staticmethod |
49 | |
50 | === modified file 'charmworld/lib/tests/test_proof.py' |
51 | --- charmworld/lib/tests/test_proof.py 2013-10-18 18:57:16 +0000 |
52 | +++ charmworld/lib/tests/test_proof.py 2013-10-22 20:55:07 +0000 |
53 | @@ -1,11 +1,6 @@ |
54 | -from mock import ( |
55 | - Mock, |
56 | - patch, |
57 | -) |
58 | -import yaml |
59 | +from mock import Mock |
60 | |
61 | from charmworld.lib.proof import ( |
62 | - BundleProof, |
63 | CharmProof, |
64 | ProofError, |
65 | ) |
66 | @@ -13,71 +8,6 @@ |
67 | from charmworld.testing import TestCase |
68 | |
69 | |
70 | -class TestBundleProof(TestCase): |
71 | - """Verify that a bundle can be proofed in charmworld.""" |
72 | - |
73 | - sample_deployer_file = """ |
74 | - { |
75 | - "wiki": { |
76 | - "services": { |
77 | - "wiki": { |
78 | - "charm": "mediawiki", |
79 | - "num_units": 2, |
80 | - "branch": "lp:charms/precise/mediawiki", |
81 | - "constraints": "mem=2" |
82 | - } |
83 | - }, |
84 | - "series": "precise" }}""" |
85 | - |
86 | - def test_check_parseable_yaml(self): |
87 | - """A deployer file should be parseable as yaml.""" |
88 | - |
89 | - deployer_file = self.sample_deployer_file |
90 | - result = BundleProof.check_parseable_deployer_file(deployer_file) |
91 | - self.assertEqual(result['wiki']['services'].keys(), ['wiki']) |
92 | - |
93 | - def test_failing_parsing_yaml_throws_proof_error(self): |
94 | - """If the yaml is not parse the yaml thows a ProofError""" |
95 | - deployer_file = '{' |
96 | - with self.assertRaises(ProofError) as exc: |
97 | - BundleProof.check_parseable_deployer_file(deployer_file) |
98 | - |
99 | - self.assertEqual( |
100 | - 'Could not parse the yaml provided.', |
101 | - exc.exception.msg |
102 | - ) |
103 | - self.assertEqual( |
104 | - deployer_file, |
105 | - exc.exception.debug_info |
106 | - ) |
107 | - |
108 | - @patch('charmworld.lib.proof.resolve_charm_from_description') |
109 | - def test_service_not_found_raises_proof_error(self, resolver): |
110 | - """If we cannot find a service specified it's a ProofError""" |
111 | - resolver.return_value = None |
112 | - deployer = yaml.load(self.sample_deployer_file) |
113 | - |
114 | - with self.assertRaises(ProofError) as exc: |
115 | - BundleProof.check_service_exists( |
116 | - None, |
117 | - 'wiki', |
118 | - deployer['wiki']['services']['wiki'], |
119 | - deployer['wiki']) |
120 | - |
121 | - self.assertEqual( |
122 | - "Could not find charm: wiki", |
123 | - exc.exception.msg |
124 | - ) |
125 | - # We should have an array of logic behind the decision to look up the |
126 | - # charm. Just check we've got some logic. This number might get |
127 | - # tweaked, but we're just checking it made it to the exception, not |
128 | - # what the reasons are. |
129 | - self.assertEqual( |
130 | - 4, |
131 | - len(exc.exception.debug_info) |
132 | - ) |
133 | - |
134 | - |
135 | class TestCharmProof(TestCase): |
136 | |
137 | def make_fake_charm_config(self, options): |
138 | @@ -130,7 +60,7 @@ |
139 | ) |
140 | |
141 | def test_check_config_fails_when_option_does_not_exist(self): |
142 | - """Bundles might attempt to set config values that are not valid""" |
143 | + """Bundles might attempt to set config values that do not exist""" |
144 | charm = Mock() |
145 | charm.options = {'foo': {}} |
146 | charm._id = 'precise/test' |
147 | |
148 | === modified file 'charmworld/models.py' |
149 | --- charmworld/models.py 2013-10-15 16:18:49 +0000 |
150 | +++ charmworld/models.py 2013-10-22 20:55:07 +0000 |
151 | @@ -19,6 +19,7 @@ |
152 | import random |
153 | import re |
154 | from xml.etree import ElementTree |
155 | +import yaml |
156 | |
157 | from bzrlib.branch import Branch |
158 | from bzrlib.errors import NoSuchRevision |
159 | @@ -27,6 +28,7 @@ |
160 | import magic |
161 | import pymongo |
162 | |
163 | +from charmworld.lib.proof import ProofError |
164 | from charmworld.lp import BASKET_SERIES |
165 | from charmworld.utils import ( |
166 | configure_logging, |
167 | @@ -1444,6 +1446,21 @@ |
168 | return hashes.keys() |
169 | |
170 | |
171 | +class DeployerParser(object): |
172 | + |
173 | + def __init__(self, data, format="yaml"): |
174 | + try: |
175 | + self.parsed = yaml.safe_load(data) |
176 | + except yaml.YAMLError, exc: |
177 | + raise ProofError( |
178 | + [ |
179 | + data, |
180 | + str(exc) |
181 | + ], |
182 | + 'Could not parse the yaml provided.' |
183 | + ) |
184 | + |
185 | + |
186 | def acquire_session_secret(database): |
187 | """Return a secret to use for AuthTkt sessions. |
188 | |
189 | @@ -1528,7 +1545,6 @@ |
190 | In the deployer, the charm key is given precedence. So we try to |
191 | mimic that by only using the charm_url if charm is not defined. |
192 | """ |
193 | - # |
194 | if not charm and charm_url: |
195 | charm = charm_url |
196 | |
197 | |
198 | === modified file 'charmworld/tests/test_models.py' |
199 | --- charmworld/tests/test_models.py 2013-10-15 16:18:49 +0000 |
200 | +++ charmworld/tests/test_models.py 2013-10-22 20:55:07 +0000 |
201 | @@ -25,6 +25,7 @@ |
202 | import pymongo |
203 | import yaml |
204 | |
205 | +from charmworld.lib.proof import ProofError |
206 | from charmworld.models import ( |
207 | acquire_session_secret, |
208 | Bundle, |
209 | @@ -34,6 +35,7 @@ |
210 | CharmFileSet, |
211 | CharmSource, |
212 | construct_charm_id, |
213 | + DeployerParser, |
214 | FeaturedSource, |
215 | find_charms, |
216 | _find_charms, |
217 | @@ -1999,6 +2001,43 @@ |
218 | self.assertIsNone(description.revision) |
219 | |
220 | |
221 | +class TestDeployerParser(TestCase): |
222 | + |
223 | + sample_deployer_file = """ |
224 | + { |
225 | + "wiki": { |
226 | + "services": { |
227 | + "wiki": { |
228 | + "charm": "mediawiki", |
229 | + "num_units": 2, |
230 | + "branch": "lp:charms/precise/mediawiki", |
231 | + "constraints": "mem=2" |
232 | + } |
233 | + }, |
234 | + "series": "precise" }}""" |
235 | + |
236 | + def test_parses_valid_yaml_file(self): |
237 | + """A deployer file should be parseable as yaml.""" |
238 | + deployer_file = self.sample_deployer_file |
239 | + deployer = DeployerParser(deployer_file) |
240 | + self.assertEqual(deployer.parsed['wiki']['services'].keys(), ['wiki']) |
241 | + |
242 | + def test_failing_parsing_yaml_throws_proof_error(self): |
243 | + """If the yaml is not parse the yaml thows a ProofError""" |
244 | + deployer_file = '{' |
245 | + with self.assertRaises(ProofError) as exc: |
246 | + DeployerParser(deployer_file) |
247 | + |
248 | + self.assertEqual( |
249 | + 'Could not parse the yaml provided.', |
250 | + exc.exception.msg |
251 | + ) |
252 | + self.assertEqual( |
253 | + deployer_file, |
254 | + exc.exception.debug_info[0] |
255 | + ) |
256 | + |
257 | + |
258 | class TestMakeBundleDoc(TestCase): |
259 | |
260 | def test_bundle_doc(self): |
261 | |
262 | === added directory 'charmworld/views/api' |
263 | === renamed file 'charmworld/views/api.py' => 'charmworld/views/api/__init__.py' |
264 | --- charmworld/views/api.py 2013-10-22 00:33:12 +0000 |
265 | +++ charmworld/views/api/__init__.py 2013-10-22 20:55:07 +0000 |
266 | @@ -6,7 +6,6 @@ |
267 | ) |
268 | from email.utils import parseaddr |
269 | from inspect import getargspec |
270 | -import json |
271 | from os.path import ( |
272 | join, |
273 | ) |
274 | @@ -22,11 +21,7 @@ |
275 | from pyramid.view import view_config |
276 | from webob import Response |
277 | |
278 | -from charmworld.lib.proof import ( |
279 | - BundleProof, |
280 | - CharmProof, |
281 | - ProofError, |
282 | -) |
283 | + |
284 | from charmworld.models import ( |
285 | Bundle, |
286 | BundledCharmDescription, |
287 | @@ -47,37 +42,8 @@ |
288 | quote_key, |
289 | timestamp, |
290 | ) |
291 | - |
292 | - |
293 | -def json_response(status, value, headers=[]): |
294 | - """Return a JSON API response. |
295 | - |
296 | - :param status: The HTTP status code to use. |
297 | - :param value: A json-serializable value to use as the body, or None for |
298 | - no body. |
299 | - """ |
300 | - # If the value is an object, then it must be representable as a mapping. |
301 | - if value is None: |
302 | - body = '' |
303 | - else: |
304 | - while True: |
305 | - try: |
306 | - body = json.dumps(value, sort_keys=True, indent=2) |
307 | - except TypeError: |
308 | - # If the serialization wasn't possible, and the value wasn't |
309 | - # already a dictionary, convert it to one and try again. |
310 | - if not isinstance(value, dict): |
311 | - value = dict(value) |
312 | - continue |
313 | - break |
314 | - |
315 | - return Response(body, |
316 | - headerlist=[ |
317 | - ('Content-Type', 'application/json'), |
318 | - ('Access-Control-Allow-Origin', '*'), |
319 | - ('Access-Control-Allow-Headers', 'X-Requested-With'), |
320 | - ] + headers, |
321 | - status_code=status) |
322 | +from charmworld.views.api.proof import proof_deployer |
323 | +from charmworld.views.utils import json_response |
324 | |
325 | |
326 | @view_config(route_name="search-json-obsolete") |
327 | @@ -494,100 +460,6 @@ |
328 | bundle = Bundle.from_query(query, self.request.db) |
329 | return bundle_id, trailing, bundle |
330 | |
331 | - def _proof_bundle(self, request): |
332 | - """Check the bundle provided for proof errors.""" |
333 | - # The top level is for general deployer file errors. |
334 | - response_data = { |
335 | - 'errors': [], |
336 | - 'error_messages': [], |
337 | - 'debug_info': [], |
338 | - } |
339 | - bundle_string = request.params.get('deployer_file') |
340 | - bundle_format = request.params.get('bundle_format', 'yaml') |
341 | - bundle_data = None |
342 | - |
343 | - if not bundle_string: |
344 | - response_data['error_messages'].append( |
345 | - 'No deployer file data received.') |
346 | - return json_response(400, response_data) |
347 | - |
348 | - try: |
349 | - bundle_data = BundleProof.check_parseable_deployer_file( |
350 | - bundle_string, |
351 | - format=bundle_format) |
352 | - except ProofError, exc: |
353 | - # If we cannot parse the config file there's nothing more to do. |
354 | - # Return immediately. |
355 | - response_data['error_messages'].append(exc.msg) |
356 | - response_data['debug_info'].append(exc.debug_info) |
357 | - return json_response(400, response_data) |
358 | - |
359 | - # Proof each bundle in the deployer file. |
360 | - for bundle_name, bundle_config in bundle_data.items(): |
361 | - bundle_info = { |
362 | - 'name': bundle_name, |
363 | - 'services': {}, |
364 | - 'relations': {}, |
365 | - } |
366 | - found_charms = {} |
367 | - |
368 | - # Verify we can find the charms at all. |
369 | - for name, charm in bundle_config.get('services').items(): |
370 | - try: |
371 | - charm_found = BundleProof.check_service_exists( |
372 | - self.request.db, name, charm, bundle_config) |
373 | - found_charms[name] = Charm(charm_found) |
374 | - except ProofError, exc: |
375 | - # We could not find the charm. No other verification can |
376 | - # happen. |
377 | - bundle_info['services'][name] = [] |
378 | - bundle_info['services'][name].append({ |
379 | - 'message': exc.msg, |
380 | - 'debug_info': exc.debug_info, |
381 | - }) |
382 | - |
383 | - # For the charms we did find in the system, verify that their |
384 | - # config is valid for the values allowed by the charm we found. |
385 | - for name, charm in found_charms.iteritems(): |
386 | - check_config = bundle_config['services'][name].get('options') |
387 | - if not check_config: |
388 | - # There's no config specified to validate. |
389 | - continue |
390 | - for test_key, test_value in check_config.iteritems(): |
391 | - try: |
392 | - CharmProof.check_config(charm, test_key, test_value) |
393 | - except ProofError, exc: |
394 | - if name not in bundle_info['services']: |
395 | - bundle_info['services'][name] = [] |
396 | - |
397 | - bundle_info['services'][name].append({ |
398 | - 'message': exc.msg, |
399 | - 'debug_info': exc.debug_info, |
400 | - }) |
401 | - |
402 | - # If there are errors in this bundles services, make sure we |
403 | - # append the info to the response data that gets sent back to the |
404 | - # user. |
405 | - if bundle_info['services'].keys(): |
406 | - response_data['errors'].append(bundle_info) |
407 | - |
408 | - # Build up a root messages list of errors that can be used to help the |
409 | - # client go through them all easily. |
410 | - error_messages = [] |
411 | - if response_data['errors']: |
412 | - for bundle in response_data['errors']: |
413 | - prefix = bundle['name'] |
414 | - for service_name, service in bundle['services'].items(): |
415 | - for err in service: |
416 | - msg = '%s: %s' % (prefix, err['message']) |
417 | - error_messages.append(msg) |
418 | - if error_messages: |
419 | - response_data['error_messages'].extend(error_messages) |
420 | - |
421 | - # After proofing each deployer file return with any failure info |
422 | - # found. |
423 | - return json_response(200, response_data) |
424 | - |
425 | def charm(self, path=None): |
426 | """Retrieve a charm according to its API ID (the path prefix).""" |
427 | if path is None: |
428 | @@ -634,7 +506,7 @@ |
429 | path = list(path) |
430 | |
431 | if path[0] == 'proof': |
432 | - return self._proof_bundle(self.request) |
433 | + return proof_deployer(self.request) |
434 | |
435 | bundle_id, trailing, bundle = self._find_bundle(path) |
436 | |
437 | |
438 | === added file 'charmworld/views/api/proof.py' |
439 | --- charmworld/views/api/proof.py 1970-01-01 00:00:00 +0000 |
440 | +++ charmworld/views/api/proof.py 2013-10-22 20:55:07 +0000 |
441 | @@ -0,0 +1,145 @@ |
442 | +from charmworld.lib.proof import ( |
443 | + CharmProof, |
444 | + ProofError, |
445 | +) |
446 | +from charmworld.models import ( |
447 | + BundledCharmDescription, |
448 | + DeployerParser, |
449 | + Charm, |
450 | + resolve_charm_from_description, |
451 | +) |
452 | +from charmworld.views.utils import json_response |
453 | + |
454 | + |
455 | +def _build_proof_response(response_data): |
456 | + # Build up a root messages list of errors that can be used to help the |
457 | + # client go through them all easily. |
458 | + error_messages = [] |
459 | + if response_data['errors']: |
460 | + for bundle in response_data['errors']: |
461 | + prefix = bundle['name'] |
462 | + for service_name, service in bundle['services'].items(): |
463 | + for err in service: |
464 | + msg = '%s: %s' % (prefix, err['message']) |
465 | + error_messages.append(msg) |
466 | + if error_messages: |
467 | + response_data['error_messages'].extend(error_messages) |
468 | + return response_data |
469 | + |
470 | + |
471 | +def _proof_bundles_charms_exist(db, bundle_info, services, series=None): |
472 | + """Verify the charms the bundle requests to deploy exist in the db.""" |
473 | + found_charms = {} |
474 | + |
475 | + # Verify we can find the charms at all. |
476 | + for name, charm in services.items(): |
477 | + try: |
478 | + charm_description = BundledCharmDescription( |
479 | + 'proof', charm, series) |
480 | + charm_found = resolve_charm_from_description( |
481 | + db, charm_description) |
482 | + if not charm_found: |
483 | + raise ProofError( |
484 | + charm_description.get_process(), |
485 | + 'Could not find charm: ' + name, |
486 | + ) |
487 | + else: |
488 | + found_charms[name] = Charm(charm_found) |
489 | + except ProofError, exc: |
490 | + # We could not find the charm. No other verification can |
491 | + # happen. |
492 | + bundle_info['services'][name] = [] |
493 | + bundle_info['services'][name].append({ |
494 | + 'message': exc.msg, |
495 | + 'debug_info': exc.debug_info, |
496 | + }) |
497 | + return found_charms, bundle_info |
498 | + |
499 | + |
500 | +def _proof_charm_config(bundle_info, bundle_config, charm_name, charm): |
501 | + """Given a charm, proof the config is valid for the charm found. |
502 | + |
503 | + If the deployer attempts to set illegal or non-existant config values to |
504 | + the charm treat these as proof errors. |
505 | + """ |
506 | + check_config = bundle_config['services'][charm_name].get('options') |
507 | + if not check_config: |
508 | + # There's no config specified to validate. |
509 | + return bundle_info |
510 | + for test_key, test_value in check_config.iteritems(): |
511 | + try: |
512 | + CharmProof.check_config(charm, test_key, test_value) |
513 | + except ProofError, exc: |
514 | + if charm_name not in bundle_info['services']: |
515 | + bundle_info['services'][charm_name] = [] |
516 | + |
517 | + bundle_info['services'][charm_name].append({ |
518 | + 'message': exc.msg, |
519 | + 'debug_info': exc.debug_info, |
520 | + }) |
521 | + return bundle_info |
522 | + |
523 | + |
524 | +def proof_deployer(request): |
525 | + """Check the bundle provided for proof errors.""" |
526 | + # The top level is for general deployer file errors. |
527 | + # The errors here may be an error about the deployer file itself, or an |
528 | + # object that describes the issues found with a bundle within the deployer |
529 | + # file. errro_messages is added as a simple aggregate of all of the error |
530 | + # messages found to make life easier for the proof cli tool. |
531 | + response_data = { |
532 | + 'errors': [], |
533 | + 'error_messages': [], |
534 | + 'debug_info': [], |
535 | + } |
536 | + deployer_string = request.params.get('deployer_file') |
537 | + deployer_format = request.params.get('deployer_format', 'yaml') |
538 | + |
539 | + # Verify we have a valid deployer file that we can find and parse. |
540 | + if not deployer_string: |
541 | + response_data['error_messages'].append( |
542 | + 'No deployer file data received.') |
543 | + return json_response(400, response_data) |
544 | + |
545 | + try: |
546 | + deployer = DeployerParser(deployer_string, format=deployer_format) |
547 | + deployer_data = deployer.parsed |
548 | + except ProofError, exc: |
549 | + # If we cannot parse the config file there's nothing more to do. |
550 | + # Return immediately. |
551 | + response_data['error_messages'].append(exc.msg) |
552 | + response_data['debug_info'].append(exc.debug_info) |
553 | + return json_response(400, response_data) |
554 | + |
555 | + # Proof each bundle found in the deployer file. |
556 | + for bundle_name, bundle_config in deployer_data.items(): |
557 | + # Track all of the issues found in this info dict. |
558 | + bundle_info = { |
559 | + 'name': bundle_name, |
560 | + 'services': {}, |
561 | + 'relations': {}, |
562 | + } |
563 | + |
564 | + # First, verify that we can locate the charms specified in this bundle |
565 | + # in the charmworld database. |
566 | + found_charms, bundle_info = _proof_bundles_charms_exist( |
567 | + request.db, |
568 | + bundle_info, |
569 | + bundle_config.get('services'), |
570 | + bundle_config.get('series')) |
571 | + |
572 | + # For the charms we did find in the system, verify that their |
573 | + # config is valid for the values allowed by the charm we found. |
574 | + for name, charm in found_charms.iteritems(): |
575 | + bundle_info = _proof_charm_config( |
576 | + bundle_info, bundle_config, name, charm) |
577 | + |
578 | + # If there are errors in this bundles services, make sure we |
579 | + # append the info to the response data that gets sent back to the |
580 | + # user. |
581 | + if bundle_info['services'].keys(): |
582 | + response_data['errors'].append(bundle_info) |
583 | + |
584 | + # After proofing each deployer file return with any failure info |
585 | + # found. |
586 | + return json_response(200, _build_proof_response(response_data)) |
587 | |
588 | === modified file 'charmworld/views/tests/test_api.py' |
589 | --- charmworld/views/tests/test_api.py 2013-10-22 00:55:23 +0000 |
590 | +++ charmworld/views/tests/test_api.py 2013-10-22 20:55:07 +0000 |
591 | @@ -9,7 +9,6 @@ |
592 | ) |
593 | import hashlib |
594 | import json |
595 | -import yaml |
596 | |
597 | from mock import patch |
598 | |
599 | @@ -41,11 +40,11 @@ |
600 | ) |
601 | from charmworld.testing import factory |
602 | from charmworld.testing import ( |
603 | - load_data_file, |
604 | ViewTestBase, |
605 | WebTestBase, |
606 | ) |
607 | from charmworld.utils import quote_key |
608 | +from charmworld.views.tests.test_proof import TestBundleProof |
609 | |
610 | |
611 | class TestObsoletedApis(WebTestBase): |
612 | @@ -868,176 +867,12 @@ |
613 | self.assertEqual(302, response.status_code) |
614 | self.assertEqual('/static/img/bundle.svg', response.location) |
615 | |
616 | - def test_bundle_proof_no_deployer_file(self): |
617 | - request = self.make_request('bundle', remainder='/proof') |
618 | - response = self.api_class(request)() |
619 | - self.assertEqual(400, response.status_code) |
620 | - self.assertEqual( |
621 | - 'No deployer file data received.', |
622 | - response.json_body['error_messages'][0]) |
623 | - |
624 | - def test_bundle_proof_invalid_deployer_file(self): |
625 | - request = self.make_request('bundle', remainder='/proof') |
626 | - request.params = { |
627 | - 'deployer_file': '{' |
628 | - } |
629 | - response = self.api_class(request)() |
630 | - self.assertEqual(400, response.status_code) |
631 | - self.assertIn( |
632 | - 'Could not parse', |
633 | - response.json_body['error_messages'][0]) |
634 | - |
635 | - def test_bundle_proof_charm_exists(self): |
636 | - deployer_file = load_data_file('sample_bundle/bundles.yaml') |
637 | - |
638 | - request = self.make_request('bundle', remainder='/proof') |
639 | - request.params = { |
640 | - 'deployer_file': deployer_file |
641 | - } |
642 | - |
643 | - response = self.api_class(request)() |
644 | - self.assertEqual(200, response.status_code) |
645 | - results = response.json_body |
646 | - self.assertEqual(1, len(results['errors'])) |
647 | - self.assertEqual(4, len(results['errors'][0]['services'].keys())) |
648 | - self.assertIn( |
649 | - 'Could not find ', |
650 | - results['errors'][0]['services']['db'][0]['message']) |
651 | - |
652 | - # And each failure has a list of 'process' information. |
653 | - self.assertIn( |
654 | - 'debug_info', |
655 | - results['errors'][0]['services']['db'][0].keys()) |
656 | - |
657 | - def test_bundle_proof_valid(self): |
658 | - # We have to create the charm so that we can verify it exists. |
659 | - _id, charm = factory.makeCharm( |
660 | - self.db, |
661 | - description='' |
662 | - ) |
663 | - |
664 | - # and we'll cheat and just find it straight by the store url. |
665 | - charm = Charm(charm) |
666 | - store_url = charm.store_url |
667 | - |
668 | - bundle_config = { |
669 | - 'wiki': { |
670 | - 'services': { |
671 | - 'charm': { |
672 | - 'charm_url': str(store_url) |
673 | - } |
674 | - } |
675 | - } |
676 | - } |
677 | - |
678 | - request = self.make_request('bundle', remainder='/proof') |
679 | - request.params = { |
680 | - 'deployer_file': yaml.dump(bundle_config) |
681 | - } |
682 | - response = self.api_class(request)() |
683 | - self.assertEqual(200, response.status_code) |
684 | - results = response.json_body |
685 | - self.assertEqual(0, len(results['errors'])) |
686 | - |
687 | - def test_bundle_proof_invalid_config_type(self): |
688 | - # We have to create the charm so that we can verify it exists. |
689 | - _id, charm = factory.makeCharm( |
690 | - self.db, |
691 | - description='' |
692 | - ) |
693 | - |
694 | - # and we'll cheat and just find it straight by the store url. |
695 | - charm = Charm(charm) |
696 | - store_url = charm.store_url |
697 | - |
698 | - bundle_config = { |
699 | - 'wiki': { |
700 | - 'services': { |
701 | - 'charm': { |
702 | - 'charm_url': str(store_url), |
703 | - 'options': { |
704 | - 'script-interval': 'invalid' |
705 | - } |
706 | - } |
707 | - } |
708 | - } |
709 | - } |
710 | - |
711 | - request = self.make_request('bundle', remainder='/proof') |
712 | - request.params = { |
713 | - 'deployer_file': yaml.dump(bundle_config) |
714 | - } |
715 | - response = self.api_class(request)() |
716 | - self.assertEqual(200, response.status_code) |
717 | - results = response.json_body |
718 | - self.assertEqual(1, len(results['errors'])) |
719 | - self.assertEqual(1, len(results['error_messages'])) |
720 | - |
721 | - # Note that the View will prefix the error message from the proof with |
722 | - # the bundle name to help identify where the error message came from. |
723 | - self.assertEqual( |
724 | - 'wiki: script-interval is not of type int.', |
725 | - results['error_messages'][0] |
726 | - ) |
727 | - |
728 | - def test_bundle_proof_supports_multiple_errors(self): |
729 | - _id, charm = factory.makeCharm( |
730 | - self.db, |
731 | - description='' |
732 | - ) |
733 | - |
734 | - # and we'll cheat and just find it straight by the store url. |
735 | - charm = Charm(charm) |
736 | - store_url = charm.store_url |
737 | - |
738 | - bundle_config = { |
739 | - 'wiki': { |
740 | - 'services': { |
741 | - 'charm': { |
742 | - 'charm_url': str(store_url), |
743 | - 'options': { |
744 | - 'script-interval': 'invalid', |
745 | - 'no-exist': 'hah invalid', |
746 | - } |
747 | - }, |
748 | - 'fail': { |
749 | - 'name': 'will-fail' |
750 | - } |
751 | - } |
752 | - } |
753 | - } |
754 | - |
755 | - request = self.make_request('bundle', remainder='/proof') |
756 | - request.params = { |
757 | - 'deployer_file': yaml.dump(bundle_config) |
758 | - } |
759 | - response = self.api_class(request)() |
760 | - self.assertEqual(200, response.status_code) |
761 | - results = response.json_body |
762 | - self.assertEqual(1, len(results['errors'])) |
763 | - self.assertEqual(3, len(results['error_messages'])) |
764 | - |
765 | - # One message from not finding a charm. |
766 | - self.assertEqual( |
767 | - 'wiki: Could not find charm: fail', |
768 | - results['error_messages'][0] |
769 | - ) |
770 | - # The other two from checking the config of the other charm. |
771 | - self.assertEqual( |
772 | - 'wiki: script-interval is not of type int.', |
773 | - results['error_messages'][1] |
774 | - ) |
775 | - self.assertEqual( |
776 | - 'wiki: The charm has no option for: no-exist', |
777 | - results['error_messages'][2] |
778 | - ) |
779 | - |
780 | - |
781 | -class TestAPI3Bundles(TestAPIBundles, API3Mixin): |
782 | + |
783 | +class TestAPI3Bundles(TestAPIBundles, API3Mixin, TestBundleProof): |
784 | """Test API 3 bundle endpoint.""" |
785 | |
786 | |
787 | -class TestAPI2Bundles(TestAPIBundles, API2Mixin): |
788 | +class TestAPI2Bundles(TestAPIBundles, API2Mixin, TestBundleProof): |
789 | """Test API 2 bundle endpoint.""" |
790 | |
791 | |
792 | |
793 | === added file 'charmworld/views/tests/test_proof.py' |
794 | --- charmworld/views/tests/test_proof.py 1970-01-01 00:00:00 +0000 |
795 | +++ charmworld/views/tests/test_proof.py 2013-10-22 20:55:07 +0000 |
796 | @@ -0,0 +1,175 @@ |
797 | +import yaml |
798 | + |
799 | +from charmworld.models import Charm |
800 | +from charmworld.testing import ( |
801 | + factory, |
802 | + load_data_file, |
803 | +) |
804 | + |
805 | + |
806 | +class TestBundleProof: |
807 | + """Verify that a bundle can be proofed in charmworld.""" |
808 | + |
809 | + def test_bundle_proof_no_deployer_file(self): |
810 | + request = self.make_request('bundle', remainder='/proof') |
811 | + response = self.api_class(request)() |
812 | + self.assertEqual(400, response.status_code) |
813 | + self.assertEqual( |
814 | + 'No deployer file data received.', |
815 | + response.json_body['error_messages'][0]) |
816 | + |
817 | + def test_bundle_proof_invalid_deployer_file(self): |
818 | + request = self.make_request('bundle', remainder='/proof') |
819 | + request.params = { |
820 | + 'deployer_file': '{' |
821 | + } |
822 | + response = self.api_class(request)() |
823 | + self.assertEqual(400, response.status_code) |
824 | + self.assertIn( |
825 | + 'Could not parse', |
826 | + response.json_body['error_messages'][0]) |
827 | + |
828 | + def test_bundle_proof_charm_exists(self): |
829 | + deployer_file = load_data_file('sample_bundle/bundles.yaml') |
830 | + |
831 | + request = self.make_request('bundle', remainder='/proof') |
832 | + request.params = { |
833 | + 'deployer_file': deployer_file |
834 | + } |
835 | + |
836 | + response = self.api_class(request)() |
837 | + self.assertEqual(200, response.status_code) |
838 | + results = response.json_body |
839 | + self.assertEqual(1, len(results['errors'])) |
840 | + self.assertEqual(4, len(results['errors'][0]['services'].keys())) |
841 | + self.assertIn( |
842 | + 'Could not find ', |
843 | + results['errors'][0]['services']['db'][0]['message']) |
844 | + |
845 | + # And each failure has a list of 'process' information. |
846 | + self.assertIn( |
847 | + 'debug_info', |
848 | + results['errors'][0]['services']['db'][0].keys()) |
849 | + |
850 | + def test_bundle_proof_valid(self): |
851 | + # We have to create the charm so that we can verify it exists. |
852 | + _id, charm = factory.makeCharm( |
853 | + self.db, |
854 | + description='' |
855 | + ) |
856 | + |
857 | + # and we'll cheat and just find it straight by the store url. |
858 | + charm = Charm(charm) |
859 | + store_url = charm.store_url |
860 | + |
861 | + bundle_config = { |
862 | + 'wiki': { |
863 | + 'services': { |
864 | + 'charm': { |
865 | + 'charm_url': str(store_url) |
866 | + } |
867 | + } |
868 | + } |
869 | + } |
870 | + |
871 | + request = self.make_request('bundle', remainder='/proof') |
872 | + request.params = { |
873 | + 'deployer_file': yaml.dump(bundle_config) |
874 | + } |
875 | + response = self.api_class(request)() |
876 | + self.assertEqual(200, response.status_code) |
877 | + results = response.json_body |
878 | + self.assertEqual(0, len(results['errors'])) |
879 | + |
880 | + def test_bundle_proof_invalid_config_type(self): |
881 | + # We have to create the charm so that we can verify it exists. |
882 | + _id, charm = factory.makeCharm( |
883 | + self.db, |
884 | + description='' |
885 | + ) |
886 | + |
887 | + # and we'll cheat and just find it straight by the store url. |
888 | + charm = Charm(charm) |
889 | + store_url = charm.store_url |
890 | + |
891 | + bundle_config = { |
892 | + 'wiki': { |
893 | + 'services': { |
894 | + 'charm': { |
895 | + 'charm_url': str(store_url), |
896 | + 'options': { |
897 | + 'script-interval': 'invalid' |
898 | + } |
899 | + } |
900 | + } |
901 | + } |
902 | + } |
903 | + |
904 | + request = self.make_request('bundle', remainder='/proof') |
905 | + request.params = { |
906 | + 'deployer_file': yaml.dump(bundle_config) |
907 | + } |
908 | + response = self.api_class(request)() |
909 | + self.assertEqual(200, response.status_code) |
910 | + results = response.json_body |
911 | + self.assertEqual(1, len(results['errors'])) |
912 | + self.assertEqual(1, len(results['error_messages'])) |
913 | + |
914 | + # Note that the View will prefix the error message from the proof with |
915 | + # the bundle name to help identify where the error message came from. |
916 | + self.assertEqual( |
917 | + 'wiki: script-interval is not of type int.', |
918 | + results['error_messages'][0] |
919 | + ) |
920 | + |
921 | + def test_bundle_proof_supports_multiple_errors(self): |
922 | + _id, charm = factory.makeCharm( |
923 | + self.db, |
924 | + description='' |
925 | + ) |
926 | + |
927 | + # and we'll cheat and just find it straight by the store url. |
928 | + charm = Charm(charm) |
929 | + store_url = charm.store_url |
930 | + |
931 | + bundle_config = { |
932 | + 'wiki': { |
933 | + 'services': { |
934 | + 'charm': { |
935 | + 'charm_url': str(store_url), |
936 | + 'options': { |
937 | + 'script-interval': 'invalid', |
938 | + 'no-exist': 'hah invalid', |
939 | + } |
940 | + }, |
941 | + 'fail': { |
942 | + 'name': 'will-fail' |
943 | + } |
944 | + } |
945 | + } |
946 | + } |
947 | + |
948 | + request = self.make_request('bundle', remainder='/proof') |
949 | + request.params = { |
950 | + 'deployer_file': yaml.dump(bundle_config) |
951 | + } |
952 | + response = self.api_class(request)() |
953 | + self.assertEqual(200, response.status_code) |
954 | + results = response.json_body |
955 | + self.assertEqual(1, len(results['errors'])) |
956 | + self.assertEqual(3, len(results['error_messages'])) |
957 | + |
958 | + # One message from not finding a charm. |
959 | + self.assertEqual( |
960 | + 'wiki: Could not find charm: fail', |
961 | + results['error_messages'][0] |
962 | + ) |
963 | + # The other two from checking the config of the other charm. |
964 | + self.assertEqual( |
965 | + 'wiki: script-interval is not of type int.', |
966 | + results['error_messages'][1] |
967 | + ) |
968 | + self.assertEqual( |
969 | + 'wiki: The charm has no option for: no-exist', |
970 | + results['error_messages'][2] |
971 | + ) |
972 | |
973 | === added file 'charmworld/views/utils.py' |
974 | --- charmworld/views/utils.py 1970-01-01 00:00:00 +0000 |
975 | +++ charmworld/views/utils.py 2013-10-22 20:55:07 +0000 |
976 | @@ -0,0 +1,33 @@ |
977 | +import json |
978 | +from webob import Response |
979 | + |
980 | + |
981 | +def json_response(status, value, headers=[]): |
982 | + """Return a JSON API response. |
983 | + |
984 | + :param status: The HTTP status code to use. |
985 | + :param value: A json-serializable value to use as the body, or None for |
986 | + no body. |
987 | + """ |
988 | + # If the value is an object, then it must be representable as a mapping. |
989 | + if value is None: |
990 | + body = '' |
991 | + else: |
992 | + while True: |
993 | + try: |
994 | + body = json.dumps(value, sort_keys=True, indent=2) |
995 | + except TypeError: |
996 | + # If the serialization wasn't possible, and the value wasn't |
997 | + # already a dictionary, convert it to one and try again. |
998 | + if not isinstance(value, dict): |
999 | + value = dict(value) |
1000 | + continue |
1001 | + break |
1002 | + |
1003 | + return Response(body, |
1004 | + headerlist=[ |
1005 | + ('Content-Type', 'application/json'), |
1006 | + ('Access-Control-Allow-Origin', '*'), |
1007 | + ('Access-Control-Allow-Headers', 'X-Requested-With'), |
1008 | + ] + headers, |
1009 | + status_code=status) |