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
=== modified file 'charmworld/lib/proof.py'
--- charmworld/lib/proof.py 2013-10-18 18:32:48 +0000
+++ charmworld/lib/proof.py 2013-10-22 20:55:07 +0000
@@ -1,10 +1,4 @@
1"""Helpers to proof charms and bundles."""1"""Helpers to proof charms and bundles."""
2import yaml
3
4from charmworld.models import (
5 BundledCharmDescription,
6 resolve_charm_from_description,
7)
82
93
10class ProofError(Exception):4class ProofError(Exception):
@@ -14,33 +8,6 @@
14 self.msg = msg8 self.msg = msg
159
1610
17class BundleProof(object):
18
19 @staticmethod
20 def check_parseable_deployer_file(data, format='yaml'):
21 try:
22 bundle_data = yaml.safe_load(data)
23 except yaml.YAMLError:
24 raise ProofError(
25 data,
26 'Could not parse the yaml provided.'
27 )
28 return bundle_data
29
30 @staticmethod
31 def check_service_exists(db, name, service_data, bundle_config):
32 charm_description = BundledCharmDescription(
33 'proof', service_data, bundle_config.get('series'))
34 charm_found = resolve_charm_from_description(db, charm_description)
35 if not charm_found:
36 raise ProofError(
37 charm_description.get_process(),
38 'Could not find charm: ' + name,
39 )
40 else:
41 return charm_found
42
43
44class CharmProof(object):11class CharmProof(object):
4512
46 @staticmethod13 @staticmethod
4714
=== modified file 'charmworld/lib/tests/test_proof.py'
--- charmworld/lib/tests/test_proof.py 2013-10-18 18:57:16 +0000
+++ charmworld/lib/tests/test_proof.py 2013-10-22 20:55:07 +0000
@@ -1,11 +1,6 @@
1from mock import (1from mock import Mock
2 Mock,
3 patch,
4)
5import yaml
62
7from charmworld.lib.proof import (3from charmworld.lib.proof import (
8 BundleProof,
9 CharmProof,4 CharmProof,
10 ProofError,5 ProofError,
11)6)
@@ -13,71 +8,6 @@
13from charmworld.testing import TestCase8from charmworld.testing import TestCase
149
1510
16class TestBundleProof(TestCase):
17 """Verify that a bundle can be proofed in charmworld."""
18
19 sample_deployer_file = """
20 {
21 "wiki": {
22 "services": {
23 "wiki": {
24 "charm": "mediawiki",
25 "num_units": 2,
26 "branch": "lp:charms/precise/mediawiki",
27 "constraints": "mem=2"
28 }
29 },
30 "series": "precise" }}"""
31
32 def test_check_parseable_yaml(self):
33 """A deployer file should be parseable as yaml."""
34
35 deployer_file = self.sample_deployer_file
36 result = BundleProof.check_parseable_deployer_file(deployer_file)
37 self.assertEqual(result['wiki']['services'].keys(), ['wiki'])
38
39 def test_failing_parsing_yaml_throws_proof_error(self):
40 """If the yaml is not parse the yaml thows a ProofError"""
41 deployer_file = '{'
42 with self.assertRaises(ProofError) as exc:
43 BundleProof.check_parseable_deployer_file(deployer_file)
44
45 self.assertEqual(
46 'Could not parse the yaml provided.',
47 exc.exception.msg
48 )
49 self.assertEqual(
50 deployer_file,
51 exc.exception.debug_info
52 )
53
54 @patch('charmworld.lib.proof.resolve_charm_from_description')
55 def test_service_not_found_raises_proof_error(self, resolver):
56 """If we cannot find a service specified it's a ProofError"""
57 resolver.return_value = None
58 deployer = yaml.load(self.sample_deployer_file)
59
60 with self.assertRaises(ProofError) as exc:
61 BundleProof.check_service_exists(
62 None,
63 'wiki',
64 deployer['wiki']['services']['wiki'],
65 deployer['wiki'])
66
67 self.assertEqual(
68 "Could not find charm: wiki",
69 exc.exception.msg
70 )
71 # We should have an array of logic behind the decision to look up the
72 # charm. Just check we've got some logic. This number might get
73 # tweaked, but we're just checking it made it to the exception, not
74 # what the reasons are.
75 self.assertEqual(
76 4,
77 len(exc.exception.debug_info)
78 )
79
80
81class TestCharmProof(TestCase):11class TestCharmProof(TestCase):
8212
83 def make_fake_charm_config(self, options):13 def make_fake_charm_config(self, options):
@@ -130,7 +60,7 @@
130 )60 )
13161
132 def test_check_config_fails_when_option_does_not_exist(self):62 def test_check_config_fails_when_option_does_not_exist(self):
133 """Bundles might attempt to set config values that are not valid"""63 """Bundles might attempt to set config values that do not exist"""
134 charm = Mock()64 charm = Mock()
135 charm.options = {'foo': {}}65 charm.options = {'foo': {}}
136 charm._id = 'precise/test'66 charm._id = 'precise/test'
13767
=== modified file 'charmworld/models.py'
--- charmworld/models.py 2013-10-15 16:18:49 +0000
+++ charmworld/models.py 2013-10-22 20:55:07 +0000
@@ -19,6 +19,7 @@
19import random19import random
20import re20import re
21from xml.etree import ElementTree21from xml.etree import ElementTree
22import yaml
2223
23from bzrlib.branch import Branch24from bzrlib.branch import Branch
24from bzrlib.errors import NoSuchRevision25from bzrlib.errors import NoSuchRevision
@@ -27,6 +28,7 @@
27import magic28import magic
28import pymongo29import pymongo
2930
31from charmworld.lib.proof import ProofError
30from charmworld.lp import BASKET_SERIES32from charmworld.lp import BASKET_SERIES
31from charmworld.utils import (33from charmworld.utils import (
32 configure_logging,34 configure_logging,
@@ -1444,6 +1446,21 @@
1444 return hashes.keys()1446 return hashes.keys()
14451447
14461448
1449class DeployerParser(object):
1450
1451 def __init__(self, data, format="yaml"):
1452 try:
1453 self.parsed = yaml.safe_load(data)
1454 except yaml.YAMLError, exc:
1455 raise ProofError(
1456 [
1457 data,
1458 str(exc)
1459 ],
1460 'Could not parse the yaml provided.'
1461 )
1462
1463
1447def acquire_session_secret(database):1464def acquire_session_secret(database):
1448 """Return a secret to use for AuthTkt sessions.1465 """Return a secret to use for AuthTkt sessions.
14491466
@@ -1528,7 +1545,6 @@
1528 In the deployer, the charm key is given precedence. So we try to1545 In the deployer, the charm key is given precedence. So we try to
1529 mimic that by only using the charm_url if charm is not defined.1546 mimic that by only using the charm_url if charm is not defined.
1530 """1547 """
1531 #
1532 if not charm and charm_url:1548 if not charm and charm_url:
1533 charm = charm_url1549 charm = charm_url
15341550
15351551
=== modified file 'charmworld/tests/test_models.py'
--- charmworld/tests/test_models.py 2013-10-15 16:18:49 +0000
+++ charmworld/tests/test_models.py 2013-10-22 20:55:07 +0000
@@ -25,6 +25,7 @@
25import pymongo25import pymongo
26import yaml26import yaml
2727
28from charmworld.lib.proof import ProofError
28from charmworld.models import (29from charmworld.models import (
29 acquire_session_secret,30 acquire_session_secret,
30 Bundle,31 Bundle,
@@ -34,6 +35,7 @@
34 CharmFileSet,35 CharmFileSet,
35 CharmSource,36 CharmSource,
36 construct_charm_id,37 construct_charm_id,
38 DeployerParser,
37 FeaturedSource,39 FeaturedSource,
38 find_charms,40 find_charms,
39 _find_charms,41 _find_charms,
@@ -1999,6 +2001,43 @@
1999 self.assertIsNone(description.revision)2001 self.assertIsNone(description.revision)
20002002
20012003
2004class TestDeployerParser(TestCase):
2005
2006 sample_deployer_file = """
2007 {
2008 "wiki": {
2009 "services": {
2010 "wiki": {
2011 "charm": "mediawiki",
2012 "num_units": 2,
2013 "branch": "lp:charms/precise/mediawiki",
2014 "constraints": "mem=2"
2015 }
2016 },
2017 "series": "precise" }}"""
2018
2019 def test_parses_valid_yaml_file(self):
2020 """A deployer file should be parseable as yaml."""
2021 deployer_file = self.sample_deployer_file
2022 deployer = DeployerParser(deployer_file)
2023 self.assertEqual(deployer.parsed['wiki']['services'].keys(), ['wiki'])
2024
2025 def test_failing_parsing_yaml_throws_proof_error(self):
2026 """If the yaml is not parse the yaml thows a ProofError"""
2027 deployer_file = '{'
2028 with self.assertRaises(ProofError) as exc:
2029 DeployerParser(deployer_file)
2030
2031 self.assertEqual(
2032 'Could not parse the yaml provided.',
2033 exc.exception.msg
2034 )
2035 self.assertEqual(
2036 deployer_file,
2037 exc.exception.debug_info[0]
2038 )
2039
2040
2002class TestMakeBundleDoc(TestCase):2041class TestMakeBundleDoc(TestCase):
20032042
2004 def test_bundle_doc(self):2043 def test_bundle_doc(self):
20052044
=== added directory 'charmworld/views/api'
=== renamed file 'charmworld/views/api.py' => 'charmworld/views/api/__init__.py'
--- charmworld/views/api.py 2013-10-22 00:33:12 +0000
+++ charmworld/views/api/__init__.py 2013-10-22 20:55:07 +0000
@@ -6,7 +6,6 @@
6)6)
7from email.utils import parseaddr7from email.utils import parseaddr
8from inspect import getargspec8from inspect import getargspec
9import json
10from os.path import (9from os.path import (
11 join,10 join,
12)11)
@@ -22,11 +21,7 @@
22from pyramid.view import view_config21from pyramid.view import view_config
23from webob import Response22from webob import Response
2423
25from charmworld.lib.proof import (24
26 BundleProof,
27 CharmProof,
28 ProofError,
29)
30from charmworld.models import (25from charmworld.models import (
31 Bundle,26 Bundle,
32 BundledCharmDescription,27 BundledCharmDescription,
@@ -47,37 +42,8 @@
47 quote_key,42 quote_key,
48 timestamp,43 timestamp,
49)44)
5045from charmworld.views.api.proof import proof_deployer
5146from charmworld.views.utils import json_response
52def json_response(status, value, headers=[]):
53 """Return a JSON API response.
54
55 :param status: The HTTP status code to use.
56 :param value: A json-serializable value to use as the body, or None for
57 no body.
58 """
59 # If the value is an object, then it must be representable as a mapping.
60 if value is None:
61 body = ''
62 else:
63 while True:
64 try:
65 body = json.dumps(value, sort_keys=True, indent=2)
66 except TypeError:
67 # If the serialization wasn't possible, and the value wasn't
68 # already a dictionary, convert it to one and try again.
69 if not isinstance(value, dict):
70 value = dict(value)
71 continue
72 break
73
74 return Response(body,
75 headerlist=[
76 ('Content-Type', 'application/json'),
77 ('Access-Control-Allow-Origin', '*'),
78 ('Access-Control-Allow-Headers', 'X-Requested-With'),
79 ] + headers,
80 status_code=status)
8147
8248
83@view_config(route_name="search-json-obsolete")49@view_config(route_name="search-json-obsolete")
@@ -494,100 +460,6 @@
494 bundle = Bundle.from_query(query, self.request.db)460 bundle = Bundle.from_query(query, self.request.db)
495 return bundle_id, trailing, bundle461 return bundle_id, trailing, bundle
496462
497 def _proof_bundle(self, request):
498 """Check the bundle provided for proof errors."""
499 # The top level is for general deployer file errors.
500 response_data = {
501 'errors': [],
502 'error_messages': [],
503 'debug_info': [],
504 }
505 bundle_string = request.params.get('deployer_file')
506 bundle_format = request.params.get('bundle_format', 'yaml')
507 bundle_data = None
508
509 if not bundle_string:
510 response_data['error_messages'].append(
511 'No deployer file data received.')
512 return json_response(400, response_data)
513
514 try:
515 bundle_data = BundleProof.check_parseable_deployer_file(
516 bundle_string,
517 format=bundle_format)
518 except ProofError, exc:
519 # If we cannot parse the config file there's nothing more to do.
520 # Return immediately.
521 response_data['error_messages'].append(exc.msg)
522 response_data['debug_info'].append(exc.debug_info)
523 return json_response(400, response_data)
524
525 # Proof each bundle in the deployer file.
526 for bundle_name, bundle_config in bundle_data.items():
527 bundle_info = {
528 'name': bundle_name,
529 'services': {},
530 'relations': {},
531 }
532 found_charms = {}
533
534 # Verify we can find the charms at all.
535 for name, charm in bundle_config.get('services').items():
536 try:
537 charm_found = BundleProof.check_service_exists(
538 self.request.db, name, charm, bundle_config)
539 found_charms[name] = Charm(charm_found)
540 except ProofError, exc:
541 # We could not find the charm. No other verification can
542 # happen.
543 bundle_info['services'][name] = []
544 bundle_info['services'][name].append({
545 'message': exc.msg,
546 'debug_info': exc.debug_info,
547 })
548
549 # For the charms we did find in the system, verify that their
550 # config is valid for the values allowed by the charm we found.
551 for name, charm in found_charms.iteritems():
552 check_config = bundle_config['services'][name].get('options')
553 if not check_config:
554 # There's no config specified to validate.
555 continue
556 for test_key, test_value in check_config.iteritems():
557 try:
558 CharmProof.check_config(charm, test_key, test_value)
559 except ProofError, exc:
560 if name not in bundle_info['services']:
561 bundle_info['services'][name] = []
562
563 bundle_info['services'][name].append({
564 'message': exc.msg,
565 'debug_info': exc.debug_info,
566 })
567
568 # If there are errors in this bundles services, make sure we
569 # append the info to the response data that gets sent back to the
570 # user.
571 if bundle_info['services'].keys():
572 response_data['errors'].append(bundle_info)
573
574 # Build up a root messages list of errors that can be used to help the
575 # client go through them all easily.
576 error_messages = []
577 if response_data['errors']:
578 for bundle in response_data['errors']:
579 prefix = bundle['name']
580 for service_name, service in bundle['services'].items():
581 for err in service:
582 msg = '%s: %s' % (prefix, err['message'])
583 error_messages.append(msg)
584 if error_messages:
585 response_data['error_messages'].extend(error_messages)
586
587 # After proofing each deployer file return with any failure info
588 # found.
589 return json_response(200, response_data)
590
591 def charm(self, path=None):463 def charm(self, path=None):
592 """Retrieve a charm according to its API ID (the path prefix)."""464 """Retrieve a charm according to its API ID (the path prefix)."""
593 if path is None:465 if path is None:
@@ -634,7 +506,7 @@
634 path = list(path)506 path = list(path)
635507
636 if path[0] == 'proof':508 if path[0] == 'proof':
637 return self._proof_bundle(self.request)509 return proof_deployer(self.request)
638510
639 bundle_id, trailing, bundle = self._find_bundle(path)511 bundle_id, trailing, bundle = self._find_bundle(path)
640512
641513
=== added file 'charmworld/views/api/proof.py'
--- charmworld/views/api/proof.py 1970-01-01 00:00:00 +0000
+++ charmworld/views/api/proof.py 2013-10-22 20:55:07 +0000
@@ -0,0 +1,145 @@
1from charmworld.lib.proof import (
2 CharmProof,
3 ProofError,
4)
5from charmworld.models import (
6 BundledCharmDescription,
7 DeployerParser,
8 Charm,
9 resolve_charm_from_description,
10)
11from charmworld.views.utils import json_response
12
13
14def _build_proof_response(response_data):
15 # Build up a root messages list of errors that can be used to help the
16 # client go through them all easily.
17 error_messages = []
18 if response_data['errors']:
19 for bundle in response_data['errors']:
20 prefix = bundle['name']
21 for service_name, service in bundle['services'].items():
22 for err in service:
23 msg = '%s: %s' % (prefix, err['message'])
24 error_messages.append(msg)
25 if error_messages:
26 response_data['error_messages'].extend(error_messages)
27 return response_data
28
29
30def _proof_bundles_charms_exist(db, bundle_info, services, series=None):
31 """Verify the charms the bundle requests to deploy exist in the db."""
32 found_charms = {}
33
34 # Verify we can find the charms at all.
35 for name, charm in services.items():
36 try:
37 charm_description = BundledCharmDescription(
38 'proof', charm, series)
39 charm_found = resolve_charm_from_description(
40 db, charm_description)
41 if not charm_found:
42 raise ProofError(
43 charm_description.get_process(),
44 'Could not find charm: ' + name,
45 )
46 else:
47 found_charms[name] = Charm(charm_found)
48 except ProofError, exc:
49 # We could not find the charm. No other verification can
50 # happen.
51 bundle_info['services'][name] = []
52 bundle_info['services'][name].append({
53 'message': exc.msg,
54 'debug_info': exc.debug_info,
55 })
56 return found_charms, bundle_info
57
58
59def _proof_charm_config(bundle_info, bundle_config, charm_name, charm):
60 """Given a charm, proof the config is valid for the charm found.
61
62 If the deployer attempts to set illegal or non-existant config values to
63 the charm treat these as proof errors.
64 """
65 check_config = bundle_config['services'][charm_name].get('options')
66 if not check_config:
67 # There's no config specified to validate.
68 return bundle_info
69 for test_key, test_value in check_config.iteritems():
70 try:
71 CharmProof.check_config(charm, test_key, test_value)
72 except ProofError, exc:
73 if charm_name not in bundle_info['services']:
74 bundle_info['services'][charm_name] = []
75
76 bundle_info['services'][charm_name].append({
77 'message': exc.msg,
78 'debug_info': exc.debug_info,
79 })
80 return bundle_info
81
82
83def proof_deployer(request):
84 """Check the bundle provided for proof errors."""
85 # The top level is for general deployer file errors.
86 # The errors here may be an error about the deployer file itself, or an
87 # object that describes the issues found with a bundle within the deployer
88 # file. errro_messages is added as a simple aggregate of all of the error
89 # messages found to make life easier for the proof cli tool.
90 response_data = {
91 'errors': [],
92 'error_messages': [],
93 'debug_info': [],
94 }
95 deployer_string = request.params.get('deployer_file')
96 deployer_format = request.params.get('deployer_format', 'yaml')
97
98 # Verify we have a valid deployer file that we can find and parse.
99 if not deployer_string:
100 response_data['error_messages'].append(
101 'No deployer file data received.')
102 return json_response(400, response_data)
103
104 try:
105 deployer = DeployerParser(deployer_string, format=deployer_format)
106 deployer_data = deployer.parsed
107 except ProofError, exc:
108 # If we cannot parse the config file there's nothing more to do.
109 # Return immediately.
110 response_data['error_messages'].append(exc.msg)
111 response_data['debug_info'].append(exc.debug_info)
112 return json_response(400, response_data)
113
114 # Proof each bundle found in the deployer file.
115 for bundle_name, bundle_config in deployer_data.items():
116 # Track all of the issues found in this info dict.
117 bundle_info = {
118 'name': bundle_name,
119 'services': {},
120 'relations': {},
121 }
122
123 # First, verify that we can locate the charms specified in this bundle
124 # in the charmworld database.
125 found_charms, bundle_info = _proof_bundles_charms_exist(
126 request.db,
127 bundle_info,
128 bundle_config.get('services'),
129 bundle_config.get('series'))
130
131 # For the charms we did find in the system, verify that their
132 # config is valid for the values allowed by the charm we found.
133 for name, charm in found_charms.iteritems():
134 bundle_info = _proof_charm_config(
135 bundle_info, bundle_config, name, charm)
136
137 # If there are errors in this bundles services, make sure we
138 # append the info to the response data that gets sent back to the
139 # user.
140 if bundle_info['services'].keys():
141 response_data['errors'].append(bundle_info)
142
143 # After proofing each deployer file return with any failure info
144 # found.
145 return json_response(200, _build_proof_response(response_data))
0146
=== modified file 'charmworld/views/tests/test_api.py'
--- charmworld/views/tests/test_api.py 2013-10-22 00:55:23 +0000
+++ charmworld/views/tests/test_api.py 2013-10-22 20:55:07 +0000
@@ -9,7 +9,6 @@
9)9)
10import hashlib10import hashlib
11import json11import json
12import yaml
1312
14from mock import patch13from mock import patch
1514
@@ -41,11 +40,11 @@
41)40)
42from charmworld.testing import factory41from charmworld.testing import factory
43from charmworld.testing import (42from charmworld.testing import (
44 load_data_file,
45 ViewTestBase,43 ViewTestBase,
46 WebTestBase,44 WebTestBase,
47)45)
48from charmworld.utils import quote_key46from charmworld.utils import quote_key
47from charmworld.views.tests.test_proof import TestBundleProof
4948
5049
51class TestObsoletedApis(WebTestBase):50class TestObsoletedApis(WebTestBase):
@@ -868,176 +867,12 @@
868 self.assertEqual(302, response.status_code)867 self.assertEqual(302, response.status_code)
869 self.assertEqual('/static/img/bundle.svg', response.location)868 self.assertEqual('/static/img/bundle.svg', response.location)
870869
871 def test_bundle_proof_no_deployer_file(self):870
872 request = self.make_request('bundle', remainder='/proof')871class TestAPI3Bundles(TestAPIBundles, API3Mixin, TestBundleProof):
873 response = self.api_class(request)()
874 self.assertEqual(400, response.status_code)
875 self.assertEqual(
876 'No deployer file data received.',
877 response.json_body['error_messages'][0])
878
879 def test_bundle_proof_invalid_deployer_file(self):
880 request = self.make_request('bundle', remainder='/proof')
881 request.params = {
882 'deployer_file': '{'
883 }
884 response = self.api_class(request)()
885 self.assertEqual(400, response.status_code)
886 self.assertIn(
887 'Could not parse',
888 response.json_body['error_messages'][0])
889
890 def test_bundle_proof_charm_exists(self):
891 deployer_file = load_data_file('sample_bundle/bundles.yaml')
892
893 request = self.make_request('bundle', remainder='/proof')
894 request.params = {
895 'deployer_file': deployer_file
896 }
897
898 response = self.api_class(request)()
899 self.assertEqual(200, response.status_code)
900 results = response.json_body
901 self.assertEqual(1, len(results['errors']))
902 self.assertEqual(4, len(results['errors'][0]['services'].keys()))
903 self.assertIn(
904 'Could not find ',
905 results['errors'][0]['services']['db'][0]['message'])
906
907 # And each failure has a list of 'process' information.
908 self.assertIn(
909 'debug_info',
910 results['errors'][0]['services']['db'][0].keys())
911
912 def test_bundle_proof_valid(self):
913 # We have to create the charm so that we can verify it exists.
914 _id, charm = factory.makeCharm(
915 self.db,
916 description=''
917 )
918
919 # and we'll cheat and just find it straight by the store url.
920 charm = Charm(charm)
921 store_url = charm.store_url
922
923 bundle_config = {
924 'wiki': {
925 'services': {
926 'charm': {
927 'charm_url': str(store_url)
928 }
929 }
930 }
931 }
932
933 request = self.make_request('bundle', remainder='/proof')
934 request.params = {
935 'deployer_file': yaml.dump(bundle_config)
936 }
937 response = self.api_class(request)()
938 self.assertEqual(200, response.status_code)
939 results = response.json_body
940 self.assertEqual(0, len(results['errors']))
941
942 def test_bundle_proof_invalid_config_type(self):
943 # We have to create the charm so that we can verify it exists.
944 _id, charm = factory.makeCharm(
945 self.db,
946 description=''
947 )
948
949 # and we'll cheat and just find it straight by the store url.
950 charm = Charm(charm)
951 store_url = charm.store_url
952
953 bundle_config = {
954 'wiki': {
955 'services': {
956 'charm': {
957 'charm_url': str(store_url),
958 'options': {
959 'script-interval': 'invalid'
960 }
961 }
962 }
963 }
964 }
965
966 request = self.make_request('bundle', remainder='/proof')
967 request.params = {
968 'deployer_file': yaml.dump(bundle_config)
969 }
970 response = self.api_class(request)()
971 self.assertEqual(200, response.status_code)
972 results = response.json_body
973 self.assertEqual(1, len(results['errors']))
974 self.assertEqual(1, len(results['error_messages']))
975
976 # Note that the View will prefix the error message from the proof with
977 # the bundle name to help identify where the error message came from.
978 self.assertEqual(
979 'wiki: script-interval is not of type int.',
980 results['error_messages'][0]
981 )
982
983 def test_bundle_proof_supports_multiple_errors(self):
984 _id, charm = factory.makeCharm(
985 self.db,
986 description=''
987 )
988
989 # and we'll cheat and just find it straight by the store url.
990 charm = Charm(charm)
991 store_url = charm.store_url
992
993 bundle_config = {
994 'wiki': {
995 'services': {
996 'charm': {
997 'charm_url': str(store_url),
998 'options': {
999 'script-interval': 'invalid',
1000 'no-exist': 'hah invalid',
1001 }
1002 },
1003 'fail': {
1004 'name': 'will-fail'
1005 }
1006 }
1007 }
1008 }
1009
1010 request = self.make_request('bundle', remainder='/proof')
1011 request.params = {
1012 'deployer_file': yaml.dump(bundle_config)
1013 }
1014 response = self.api_class(request)()
1015 self.assertEqual(200, response.status_code)
1016 results = response.json_body
1017 self.assertEqual(1, len(results['errors']))
1018 self.assertEqual(3, len(results['error_messages']))
1019
1020 # One message from not finding a charm.
1021 self.assertEqual(
1022 'wiki: Could not find charm: fail',
1023 results['error_messages'][0]
1024 )
1025 # The other two from checking the config of the other charm.
1026 self.assertEqual(
1027 'wiki: script-interval is not of type int.',
1028 results['error_messages'][1]
1029 )
1030 self.assertEqual(
1031 'wiki: The charm has no option for: no-exist',
1032 results['error_messages'][2]
1033 )
1034
1035
1036class TestAPI3Bundles(TestAPIBundles, API3Mixin):
1037 """Test API 3 bundle endpoint."""872 """Test API 3 bundle endpoint."""
1038873
1039874
1040class TestAPI2Bundles(TestAPIBundles, API2Mixin):875class TestAPI2Bundles(TestAPIBundles, API2Mixin, TestBundleProof):
1041 """Test API 2 bundle endpoint."""876 """Test API 2 bundle endpoint."""
1042877
1043878
1044879
=== added file 'charmworld/views/tests/test_proof.py'
--- charmworld/views/tests/test_proof.py 1970-01-01 00:00:00 +0000
+++ charmworld/views/tests/test_proof.py 2013-10-22 20:55:07 +0000
@@ -0,0 +1,175 @@
1import yaml
2
3from charmworld.models import Charm
4from charmworld.testing import (
5 factory,
6 load_data_file,
7)
8
9
10class TestBundleProof:
11 """Verify that a bundle can be proofed in charmworld."""
12
13 def test_bundle_proof_no_deployer_file(self):
14 request = self.make_request('bundle', remainder='/proof')
15 response = self.api_class(request)()
16 self.assertEqual(400, response.status_code)
17 self.assertEqual(
18 'No deployer file data received.',
19 response.json_body['error_messages'][0])
20
21 def test_bundle_proof_invalid_deployer_file(self):
22 request = self.make_request('bundle', remainder='/proof')
23 request.params = {
24 'deployer_file': '{'
25 }
26 response = self.api_class(request)()
27 self.assertEqual(400, response.status_code)
28 self.assertIn(
29 'Could not parse',
30 response.json_body['error_messages'][0])
31
32 def test_bundle_proof_charm_exists(self):
33 deployer_file = load_data_file('sample_bundle/bundles.yaml')
34
35 request = self.make_request('bundle', remainder='/proof')
36 request.params = {
37 'deployer_file': deployer_file
38 }
39
40 response = self.api_class(request)()
41 self.assertEqual(200, response.status_code)
42 results = response.json_body
43 self.assertEqual(1, len(results['errors']))
44 self.assertEqual(4, len(results['errors'][0]['services'].keys()))
45 self.assertIn(
46 'Could not find ',
47 results['errors'][0]['services']['db'][0]['message'])
48
49 # And each failure has a list of 'process' information.
50 self.assertIn(
51 'debug_info',
52 results['errors'][0]['services']['db'][0].keys())
53
54 def test_bundle_proof_valid(self):
55 # We have to create the charm so that we can verify it exists.
56 _id, charm = factory.makeCharm(
57 self.db,
58 description=''
59 )
60
61 # and we'll cheat and just find it straight by the store url.
62 charm = Charm(charm)
63 store_url = charm.store_url
64
65 bundle_config = {
66 'wiki': {
67 'services': {
68 'charm': {
69 'charm_url': str(store_url)
70 }
71 }
72 }
73 }
74
75 request = self.make_request('bundle', remainder='/proof')
76 request.params = {
77 'deployer_file': yaml.dump(bundle_config)
78 }
79 response = self.api_class(request)()
80 self.assertEqual(200, response.status_code)
81 results = response.json_body
82 self.assertEqual(0, len(results['errors']))
83
84 def test_bundle_proof_invalid_config_type(self):
85 # We have to create the charm so that we can verify it exists.
86 _id, charm = factory.makeCharm(
87 self.db,
88 description=''
89 )
90
91 # and we'll cheat and just find it straight by the store url.
92 charm = Charm(charm)
93 store_url = charm.store_url
94
95 bundle_config = {
96 'wiki': {
97 'services': {
98 'charm': {
99 'charm_url': str(store_url),
100 'options': {
101 'script-interval': 'invalid'
102 }
103 }
104 }
105 }
106 }
107
108 request = self.make_request('bundle', remainder='/proof')
109 request.params = {
110 'deployer_file': yaml.dump(bundle_config)
111 }
112 response = self.api_class(request)()
113 self.assertEqual(200, response.status_code)
114 results = response.json_body
115 self.assertEqual(1, len(results['errors']))
116 self.assertEqual(1, len(results['error_messages']))
117
118 # Note that the View will prefix the error message from the proof with
119 # the bundle name to help identify where the error message came from.
120 self.assertEqual(
121 'wiki: script-interval is not of type int.',
122 results['error_messages'][0]
123 )
124
125 def test_bundle_proof_supports_multiple_errors(self):
126 _id, charm = factory.makeCharm(
127 self.db,
128 description=''
129 )
130
131 # and we'll cheat and just find it straight by the store url.
132 charm = Charm(charm)
133 store_url = charm.store_url
134
135 bundle_config = {
136 'wiki': {
137 'services': {
138 'charm': {
139 'charm_url': str(store_url),
140 'options': {
141 'script-interval': 'invalid',
142 'no-exist': 'hah invalid',
143 }
144 },
145 'fail': {
146 'name': 'will-fail'
147 }
148 }
149 }
150 }
151
152 request = self.make_request('bundle', remainder='/proof')
153 request.params = {
154 'deployer_file': yaml.dump(bundle_config)
155 }
156 response = self.api_class(request)()
157 self.assertEqual(200, response.status_code)
158 results = response.json_body
159 self.assertEqual(1, len(results['errors']))
160 self.assertEqual(3, len(results['error_messages']))
161
162 # One message from not finding a charm.
163 self.assertEqual(
164 'wiki: Could not find charm: fail',
165 results['error_messages'][0]
166 )
167 # The other two from checking the config of the other charm.
168 self.assertEqual(
169 'wiki: script-interval is not of type int.',
170 results['error_messages'][1]
171 )
172 self.assertEqual(
173 'wiki: The charm has no option for: no-exist',
174 results['error_messages'][2]
175 )
0176
=== added file 'charmworld/views/utils.py'
--- charmworld/views/utils.py 1970-01-01 00:00:00 +0000
+++ charmworld/views/utils.py 2013-10-22 20:55:07 +0000
@@ -0,0 +1,33 @@
1import json
2from webob import Response
3
4
5def json_response(status, value, headers=[]):
6 """Return a JSON API response.
7
8 :param status: The HTTP status code to use.
9 :param value: A json-serializable value to use as the body, or None for
10 no body.
11 """
12 # If the value is an object, then it must be representable as a mapping.
13 if value is None:
14 body = ''
15 else:
16 while True:
17 try:
18 body = json.dumps(value, sort_keys=True, indent=2)
19 except TypeError:
20 # If the serialization wasn't possible, and the value wasn't
21 # already a dictionary, convert it to one and try again.
22 if not isinstance(value, dict):
23 value = dict(value)
24 continue
25 break
26
27 return Response(body,
28 headerlist=[
29 ('Content-Type', 'application/json'),
30 ('Access-Control-Allow-Origin', '*'),
31 ('Access-Control-Allow-Headers', 'X-Requested-With'),
32 ] + headers,
33 status_code=status)

Subscribers

People subscribed via source and target branches