Merge lp:~rharding/charmworld/proof-config-2 into lp:~juju-jitsu/charmworld/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
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.

https://codereview.appspot.com/15690049/

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)

Subscribers

People subscribed via source and target branches