Merge lp:~bac/charmworld/bundles-display into lp:~juju-jitsu/charmworld/trunk

Proposed by Brad Crittenden
Status: Merged
Approved by: Brad Crittenden
Approved revision: 348
Merged at revision: 344
Proposed branch: lp:~bac/charmworld/bundles-display
Merge into: lp:~juju-jitsu/charmworld/trunk
Diff against target: 553 lines (+173/-74)
9 files modified
charmworld/jobs/ingest.py (+10/-8)
charmworld/jobs/tests/test_ingest.py (+15/-11)
charmworld/models.py (+50/-14)
charmworld/search.py (+4/-3)
charmworld/testing/factory.py (+19/-7)
charmworld/tests/test_models.py (+19/-8)
charmworld/tests/test_search.py (+35/-0)
charmworld/views/api.py (+6/-5)
charmworld/views/tests/test_api.py (+15/-18)
To merge this branch: bzr merge lp:~bac/charmworld/bundles-display
Reviewer Review Type Date Requested Status
Benji York (community) Approve
Review via email: mp+179541@code.launchpad.net

Commit message

Change the basket field in the Bundle to be a basket name and revision number in the form 'basket/revno'.

Description of the change

Change the basket field in the Bundle to be a basket name and revision number in the form 'basket/revno'. Previously there was inconsistency in the way the basket id was created and used.

Remove from the top-level any bundle items that are in the 'data' dictionary.

Change models.py:store_bundles to be easier to test by making the collection and indexer optional.

To post a comment you must log in.
Revision history for this message
Benji York (benji) wrote :

The branch looks good. Here are some semi-bikesheddy comments.

It's kind of strange that construct_basket_id returns two values while
all the other "construct_*_id" functions just return the ID. Perhaps
two functions would be better. (?)

Does "make lint" complain about the new unused variables?

Re. get_bundle_data: so now if you don't specify an owner argument you
get an empty string, but if you pass owner=None you get a random string?
That seems a little odd. Oh, maybe you did that because the change in
behavior broke a bunch of tests.

Another thing, about get_bundle_data: I always thought it was weird to
use the local namespace as a sandbox in which to construct the return
value. Now that you're doing more in the function and having to do the
"del rep[k]" at the end it's getting more strained. You might consider
revamping the function to use **charm_data as the parameter list instead
and then manipulating charm_data instead of the locals.

review: Approve
Revision history for this message
Brad Crittenden (bac) wrote :

> The branch looks good. Here are some semi-bikesheddy comments.
>
> It's kind of strange that construct_basket_id returns two values while
> all the other "construct_*_id" functions just return the ID. Perhaps
> two functions would be better. (?)

I renamed to set_basket_info and modify the basket_data dictionary in place.

>
> Does "make lint" complain about the new unused variables?

Nope. Which ones?

>
>
> Re. get_bundle_data: so now if you don't specify an owner argument you
> get an empty string, but if you pass owner=None you get a random string?

No, both cases give you a random string.

> That seems a little odd. Oh, maybe you did that because the change in
> behavior broke a bunch of tests.

No, any broken tests were fixed.

>
> Another thing, about get_bundle_data: I always thought it was weird to
> use the local namespace as a sandbox in which to construct the return
> value. Now that you're doing more in the function and having to do the
> "del rep[k]" at the end it's getting more strained. You might consider
> revamping the function to use **charm_data as the parameter list instead
> and then manipulating charm_data instead of the locals.

I've fixed it to be more explicit.

Thanks for the review and good suggestions.

Revision history for this message
Charmworld Lander (charmworld-lander) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charmworld/jobs/ingest.py'
2--- charmworld/jobs/ingest.py 2013-08-09 18:53:40 +0000
3+++ charmworld/jobs/ingest.py 2013-08-11 23:10:56 +0000
4@@ -288,13 +288,14 @@
5 self.working_dir = working_dir
6 super(UpdateBundleJob, self).__init__()
7
8- def store_bundles(self, deployer_config, basket_id):
9- store_bundles(self.db.bundles, deployer_config, basket_id)
10+ def store_bundles(self, deployer_config, owner, basket_id):
11+ store_bundles(self.db.bundles, deployer_config, owner, basket_id)
12
13 @staticmethod
14- def construct_basket_id(basket_data, revno):
15- owner_segment = '~%s/' % basket_data['owner']
16- return '%s%s-%d' % (owner_segment, basket_data['name'], revno)
17+ def set_basket_info(data, revno):
18+ owner_segment = '~%s' % data['owner']
19+ data['name_revno'] = "%s/%d" % (data['name'], revno)
20+ data['_id'] = '%s/%s' % (owner_segment, data['name_revno'])
21
22 @staticmethod
23 def decorate_bundles(basket_contents, basket_id):
24@@ -306,7 +307,7 @@
25 branch_dir = fetch_branch(self.working_dir, basket_data, self.log)
26 branch = Branch.open(branch_dir)
27 revno = branch.revision_id_to_revno(basket_data['commit'])
28- basket_data['_id'] = self.construct_basket_id(basket_data, revno)
29+ self.set_basket_info(basket_data, revno)
30 with read_locked(branch):
31 tree = branch.repository.revision_tree(basket_data['commit'])
32 basket_data['file_hashes'] = quote_yaml(slurp_files(fs, tree))
33@@ -324,7 +325,8 @@
34 self.db.baskets.save(basket_data)
35 deployer_config = self.get_deployer_config(fs, basket_data)
36 self.decorate_bundles(deployer_config, basket_data['_id'])
37- self.store_bundles(deployer_config, basket_data['_id'])
38+ self.store_bundles(
39+ deployer_config, basket_data['owner'], basket_data['name_revno'])
40
41
42 def _rev_info(r, branch):
43@@ -333,7 +335,7 @@
44 "revno": branch.revision_id_to_revno(r.revision_id),
45 "committer": r.committer,
46 "created": r.timestamp,
47- "message": r.message
48+ "message": r.message,
49 }
50 return d
51
52
53=== modified file 'charmworld/jobs/tests/test_ingest.py'
54--- charmworld/jobs/tests/test_ingest.py 2013-08-09 18:53:40 +0000
55+++ charmworld/jobs/tests/test_ingest.py 2013-08-11 23:10:56 +0000
56@@ -383,7 +383,7 @@
57 with bundle_update_environment(source_branch_dir):
58 job.decorate_basket(bundle_data, None)
59
60- self.assertTrue(bundle_data['_id'].endswith('-8'))
61+ self.assertTrue(bundle_data['_id'].endswith('/8'))
62
63 def test_decorate_basket_stores_files(self):
64 job = UpdateBundleJob()
65@@ -423,21 +423,22 @@
66 with self.assertRaises(bzrlib.errors.NoSuchRevision):
67 job.decorate_basket(bundle_data, None)
68
69- def test_construct_basket_id_includes_owner_if_promulgated(self):
70+ def test_set_basket_info_includes_owner_if_promulgated(self):
71 payload = factory.get_payload_json(name='mysql', promulgated=True)
72- basket_id = UpdateBundleJob.construct_basket_id(payload, 5)
73- self.assertEqual('~charmers/mysql-5', basket_id)
74+ UpdateBundleJob.set_basket_info(payload, 5)
75+ self.assertEqual('mysql/5', payload['name_revno'])
76+ self.assertEqual('~charmers/mysql/5', payload['_id'])
77
78- def test_construct_basket_id_includes_owner_if_not_promulgated(self):
79+ def test_set_basket_info_includes_owner_if_not_promulgated(self):
80 payload = factory.get_payload_json(name='mysql', promulgated=False)
81- basket_id = UpdateBundleJob.construct_basket_id(payload, 5)
82- self.assertEqual('~charmers/mysql-5', basket_id)
83+ UpdateBundleJob.set_basket_info(payload, 5)
84+ self.assertEqual('mysql/5', payload['name_revno'])
85+ self.assertEqual('~charmers/mysql/5', payload['_id'])
86
87 def test_job_can_store_bundles_in_the_db(self):
88 basket_data = factory.get_payload_json(name='wordpress')
89- basket_id = UpdateBundleJob.construct_basket_id(basket_data, 5)
90- bundle_id = '%s/%s' % (basket_id, 'stage')
91- basket_data['_id'] = basket_id
92+ UpdateBundleJob.set_basket_info(basket_data, 5)
93+ bundle_id = '%s/%s' % (basket_data['_id'], 'stage')
94 job = UpdateBundleJob()
95 job.setup(self.db)
96 DEPLOYER_CONFIG_HASH = '31337'
97@@ -457,11 +458,14 @@
98 engine: apache
99 """)
100 fs.put(deployer_config, _id=DEPLOYER_CONFIG_HASH)
101- job.store_bundles(yaml.safe_load(deployer_config), basket_id)
102+ job.store_bundles(
103+ yaml.safe_load(deployer_config),
104+ basket_data['owner'], basket_data['name_revno'])
105 self.assertIsNotNone(self.db.bundles.find_one(bundle_id))
106
107 def test_job_run_stores_bundles_in_the_db(self):
108 basket_data = factory.get_payload_json(name='wordpress')
109+ basket_data['name_revno'] = 'dummy'
110 job = UpdateBundleJob()
111 job.setup(self.db)
112 with patch.object(job, 'get_deployer_config'):
113
114=== modified file 'charmworld/models.py'
115--- charmworld/models.py 2013-08-09 18:53:40 +0000
116+++ charmworld/models.py 2013-08-11 23:10:56 +0000
117@@ -7,14 +7,17 @@
118 from bzrlib.branch import Branch
119 from bzrlib.errors import NoSuchRevision
120 from calendar import timegm
121+import copy
122 import hashlib
123 import logging
124 from mimetypes import guess_type
125 import os
126-from os.path import dirname
127-from os.path import join
128-from os.path import normpath
129-from os.path import split
130+from os.path import (
131+ dirname,
132+ join,
133+ normpath,
134+ split,
135+)
136 import random
137 import re
138
139@@ -1176,11 +1179,9 @@
140 'basket': None,
141 'name': '',
142 'inherits': None,
143- 'series': '',
144 'title': '',
145 'description': '',
146- 'services': dict(),
147- 'relations': dict(),
148+ 'data': dict(),
149 'promulgated': False,
150 'branch_deleted': False,
151 'doctype': 'bundle',
152@@ -1204,9 +1205,13 @@
153 else:
154 raise AttributeError
155
156+ @staticmethod
157+ def construct_id(owner, basket, name):
158+ return "~%s/%s/%s" % (owner, basket, name)
159+
160 @property
161 def id(self):
162- return "~{}/{}/{}".format(self.owner, self.basket, self.name)
163+ return self.construct_id(self.owner, self.basket, self.name)
164
165 @property
166 def permanent_url(self):
167@@ -1303,14 +1308,45 @@
168 return options
169
170
171-def store_bundles(collection, deployer_config, basket_id):
172- """Store a basket of bundles into both MongoDB and ElasticSearch."""
173- index_client = ElasticSearchClient.from_settings(settings)
174- index_client.index_bundles(deployer_config.values())
175+def store_bundles(collection, deployer_config, owner, basket_id,
176+ index_client=None):
177+ """Store a basket of bundles into MongoDB and/or ElasticSearch.
178+
179+ :param: collection: A db bundles collection. If None the bundles are not
180+ stored to Mongo.
181+ :param: deployer_config: A dictionary representing the basket of bundles.
182+ :param: owner: A string representing the owner. No leading ~.
183+ :param: basket_id: A slash-joined string of name and revision for the
184+ basket. Does not include the owner name.
185+ :param: index_client: The elastic search client. If None it will be
186+ retrieved. Useful for Mongo-only tests where the indexing is not
187+ needed.
188+ """
189+
190+ if index_client is None:
191+ index_client = ElasticSearchClient.from_settings(settings)
192+
193+ # Augment the deployer_config with additional information for indexing.
194+ ids = {}
195+ config_copy = copy.deepcopy(deployer_config)
196+ for key in config_copy:
197+ ids[key] = Bundle.construct_id(owner, basket_id, key)
198+ config_copy[key]['id'] = ids[key]
199+ config_copy[key]['owner'] = owner
200+ config_copy[key]['name'] = key
201+ config_copy[key]['basket'] = basket_id
202+
203+ index_client.index_bundles(config_copy.values())
204+ if collection is None:
205+ return
206 for key in deployer_config:
207+ data = get_flattened_deployment(deployer_config, key)
208 bundle_doc = {
209- '_id': '%s/%s' % (basket_id, key),
210- 'data': get_flattened_deployment(deployer_config, key),
211+ '_id': ids[key],
212+ 'name': key,
213+ 'owner': owner,
214+ 'basket': basket_id,
215+ 'data': data,
216 }
217 collection.save(bundle_doc)
218
219
220=== modified file 'charmworld/search.py'
221--- charmworld/search.py 2013-08-09 18:53:40 +0000
222+++ charmworld/search.py 2013-08-11 23:10:56 +0000
223@@ -32,6 +32,7 @@
224 'description': 3,
225 'config.options.description': None,
226 'relations': None,
227+ 'services': None,
228 }
229
230
231@@ -430,9 +431,9 @@
232
233 with Timer() as timer:
234 status = self.get_status()
235- for real_index in self.get_aliased():
236- break
237- else:
238+ try:
239+ real_index = self.get_aliased()[0]
240+ except IndexError:
241 real_index = self.index_name
242 docs = status[real_index].get('docs')
243 if docs is None:
244
245=== modified file 'charmworld/testing/factory.py'
246--- charmworld/testing/factory.py 2013-08-09 18:53:40 +0000
247+++ charmworld/testing/factory.py 2013-08-11 23:10:56 +0000
248@@ -11,7 +11,6 @@
249 )
250 import hashlib
251 import os
252-from os.path import basename
253 from random import randint
254 import random
255 import stat
256@@ -311,17 +310,29 @@
257 return record_id, charm
258
259
260-def get_bundle_data(name=None, owner='', basket=None, inherits=None,
261+def get_bundle_data(name=None, owner=None, basket=None, inherits=None,
262 series='precise', title='', description='',
263 services=dict(), relations=dict(), promulgated=False,
264 branch_deleted=False):
265 if name is None:
266 name = random_string(10)
267+ if owner is None:
268+ owner = random_string(10)
269 if basket is None:
270- basket = random_string(10)
271- doctype = 'bundle'
272- doctype # Hush lint's dislike of defining variables for the vars() call.
273- return dict(**vars())
274+ basket = "%s/%d" % (random_string(10), randint(0, 100))
275+ data = dict(series=series,
276+ relations=relations,
277+ services=services)
278+ return dict(basket=basket,
279+ branch_deleted=branch_deleted,
280+ data=data,
281+ description=description,
282+ doctype='bundle',
283+ inherits=inherits,
284+ name=name,
285+ owner=owner,
286+ promulgated=promulgated,
287+ title=title)
288
289
290 def makeBundle(db, *args, **kwargs):
291@@ -334,7 +345,8 @@
292
293 def make_charm_file(db, charm, path, content=None):
294 fs = getfs(db)
295- charm_file = CharmFileSet.make_charm_file(fs, charm, path, basename(path))
296+ charm_file = CharmFileSet.make_charm_file(
297+ fs, charm, path, os.path.basename(path))
298 if content is None:
299 content = random_string(10).encode('utf-8')
300 charm_file.save(content)
301
302=== modified file 'charmworld/tests/test_models.py'
303--- charmworld/tests/test_models.py 2013-08-09 18:53:40 +0000
304+++ charmworld/tests/test_models.py 2013-08-11 23:10:56 +0000
305@@ -1458,6 +1458,7 @@
306
307 def test_store_bundles(self):
308 # The bundles can be stored in the database.
309+
310 deployer_config = dedent("""\
311 wordpress-stage:
312 series: precise
313@@ -1470,13 +1471,21 @@
314 engine: apache
315 """)
316 parsed = yaml.safe_load(deployer_config)
317- store_bundles(self.db.bundles, parsed, 'wordpress-5')
318+ owner = 'bac'
319+ bundle_name = 'wordpress-stage'
320+ basket_id = 'wordpress-basket/5'
321+ _id = "~%s/%s/%s" % (owner, basket_id, bundle_name)
322+ store_bundles(
323+ self.db.bundles, parsed, 'bac', basket_id)
324 self.assertEqual(
325 {
326- '_id': 'wordpress-5/wordpress-stage',
327+ '_id': _id,
328+ 'basket': basket_id,
329+ 'name': bundle_name,
330+ 'owner': owner,
331 'data': parsed['wordpress-stage'],
332 },
333- self.db.bundles.find_one('wordpress-5/wordpress-stage'))
334+ self.db.bundles.find_one(_id))
335
336 def test_store_bundles_calls_get_flattened_deployment(self):
337 # For every key in the top-level deployment data,
338@@ -1509,7 +1518,7 @@
339
340 with patch('charmworld.models.get_flattened_deployment',
341 get_flattened_deployment):
342- store_bundles(self.db.bundles, parsed, 'wordpress-5')
343+ store_bundles(self.db.bundles, parsed, 'bac', 'wordpress-basket/5')
344 self.assertItemsEqual(['wordpress-stage', 'wordpress-prod'], keys)
345
346 def test_storing_a_bundle_includes_indexing_it(self):
347@@ -1527,7 +1536,7 @@
348 with patch(
349 'charmworld.models.ElasticSearchClient',
350 FauxElasticSearchClient):
351- store_bundles(self.db.bundles, {}, 'wordpress-5')
352+ store_bundles(self.db.bundles, {}, 'owner', 'wordpress-basket/5')
353
354 self.assertTrue(FauxElasticSearchClient.index_bundles_called)
355
356@@ -1547,12 +1556,14 @@
357 'basket': 'mysql',
358 'name': 'tiny',
359 'inherits': 'main-mysql',
360- 'series': 'series',
361 'title': 'Tiny bundle',
362 'description': 'My Tiny Bundle',
363 'doctype': 'bundle',
364- 'services': dict(apache=1),
365- 'relations': dict(a=1),
366+ 'data': {
367+ 'services': dict(apache=1),
368+ 'relations': dict(a=1),
369+ 'series': 'series',
370+ },
371 'promulgated': True,
372 'branch_deleted': True,
373 }
374
375=== modified file 'charmworld/tests/test_search.py'
376--- charmworld/tests/test_search.py 2013-08-09 18:53:40 +0000
377+++ charmworld/tests/test_search.py 2013-08-11 23:10:56 +0000
378@@ -5,12 +5,14 @@
379
380
381 from datetime import datetime
382+from textwrap import dedent
383
384 from pyelasticsearch import ElasticSearch
385 from pyelasticsearch.exceptions import (
386 ElasticHttpError,
387 ElasticHttpNotFoundError,
388 )
389+import yaml
390
391 from charmworld.charmstore import (
392 get_address,
393@@ -19,6 +21,7 @@
394 from charmworld.models import (
395 Bundle,
396 Charm,
397+ store_bundles,
398 )
399 from charmworld.search import (
400 BUNDLE,
401@@ -146,6 +149,38 @@
402 self.assertEqual(1, len(results))
403 self.assertEqual(bundle, results[0]['data'])
404
405+ def test_store_bundles(self):
406+ # Ensure that bundles stored into the index via the `store_bundles`
407+ # function are searchable via the index.
408+ _id = '~abentley/wordpress-basket/5/wordpress-stage'
409+ deployer_config = dedent("""\
410+ wordpress-stage:
411+ series: precise
412+ services:
413+ blog:
414+ charm: cs:precise/wordpress
415+ constraints: mem=2
416+ options:
417+ tuning: optimized
418+ engine: apache
419+ """)
420+ parsed = yaml.safe_load(deployer_config)
421+ client = self.index_client
422+ store_bundles(None, parsed, 'abentley', 'wordpress-basket/5',
423+ index_client=client)
424+ # Bundle name is indexed.
425+ results = client.search('wordpress-stage')['results']['bundle']
426+ self.assertEqual(1, len(results))
427+ self.assertEqual(_id, results[0]['data'].id)
428+ # Series is indexed.
429+ results = client.search('precise')['results']['bundle']
430+ self.assertEqual(1, len(results))
431+ self.assertEqual(_id, results[0]['data'].id)
432+ # Owner is indexed.
433+ results = client.search('abentley')['results']['bundle']
434+ self.assertEqual(1, len(results))
435+ self.assertEqual(_id, results[0]['data'].id)
436+
437 def test_search_charms_and_bundles_same_name(self):
438 charm = Charm(self.makeCharm(name='mozilla'))
439 bundle = Bundle(self.makeBundle(name='mozilla'))
440
441=== modified file 'charmworld/views/api.py'
442--- charmworld/views/api.py 2013-08-07 13:31:20 +0000
443+++ charmworld/views/api.py 2013-08-11 23:10:56 +0000
444@@ -351,11 +351,12 @@
445 if path is None:
446 raise HTTPNotFound(self.request.path)
447 fullpath = '/'.join(path)
448- if len(path) == 3:
449+ if len(path) == 4:
450 # We have an owner.
451 query = {'_id': fullpath}
452- elif len(path) == 2:
453- query = {'basket': path[0], 'name': path[1], 'promulgated': True}
454+ elif len(path) == 3:
455+ basket_id = join(path[0], path[1])
456+ query = {'basket': basket_id, 'name': path[2], 'promulgated': True}
457 else:
458 raise HTTPNotFound(self.request.path)
459 bundle_data = self.request.db.bundles.find_one(query)
460@@ -363,7 +364,7 @@
461 return json_response(
462 404, {'type': 'no_such_bundle', 'bundle_id': fullpath})
463 bundle = Bundle(bundle_data)
464- return bundle._representation # wrong
465+ return {bundle.name: bundle.data}
466
467 @staticmethod
468 def _get_file_headers(md5sum, content_type=None):
469@@ -371,7 +372,7 @@
470 ("Access-Control-Allow-Origin", "*"),
471 ("Access-Control-Allow-Headers", "X-Requested-With"),
472 ("Cache-Control", "max-age=86400, public"),
473- ("Etag", '"%s"' % md5sum)
474+ ("Etag", '"%s"' % md5sum),
475 ]
476 if content_type is not None:
477 headerlist.append(('Content-Type', content_type))
478
479=== modified file 'charmworld/views/tests/test_api.py'
480--- charmworld/views/tests/test_api.py 2013-08-09 15:46:51 +0000
481+++ charmworld/views/tests/test_api.py 2013-08-11 23:10:56 +0000
482@@ -635,15 +635,16 @@
483 bundle = self.makeBundle()
484 expected = dict(
485 name=bundle.name,
486- owner='',
487+ owner=bundle.owner,
488 basket=bundle.basket,
489 inherits=None,
490- series='precise',
491 title='',
492+ data=dict(services=dict(),
493+ relations=dict(),
494+ series='precise',
495+ ),
496 description='',
497 doctype='bundle',
498- services=dict(),
499- relations=dict(),
500 promulgated=False,
501 branch_deleted=False,
502 )
503@@ -667,7 +668,7 @@
504
505 def test_not_found(self):
506 self.makeBundle(name='bat')
507- response = self.get_response('bundle', 'foo/bar')
508+ response = self.get_response('bundle', 'foo/4/bar')
509 self.assertEqual(404, response.status_code)
510
511 def test_no_path(self):
512@@ -675,32 +676,28 @@
513 self.get_response('bundle', remainder=None)
514
515 def test_valid_lookup_unpromulgated(self):
516- self.makeBundle(name='bat', owner='bac', basket='byobu-4')
517- response = self.get_response('bundle', '~bac/byobu-4/bat')
518+ self.makeBundle(name='bat', owner='bac', basket='byobu/4')
519+ response = self.get_response('bundle', '~bac/byobu/4/bat')
520 self.assertEqual(200, response.status_code)
521- response = self.get_response('bundle', 'byobu-4/bat')
522+ response = self.get_response('bundle', 'byobu/4/bat')
523 self.assertEqual(404, response.status_code)
524
525 def test_valid_lookup_promulgated(self):
526 self.makeBundle(
527- name='bat', owner='bac', basket='byobu-4',
528+ name='bat', owner='bac', basket='byobu/4',
529 promulgated=True)
530 # Can look up via owner-based id and short id.
531- response = self.get_response('bundle', '~bac/byobu-4/bat')
532+ response = self.get_response('bundle', '~bac/byobu/4/bat')
533 self.assertEqual(200, response.status_code)
534- response = self.get_response('bundle', 'byobu-4/bat')
535+ response = self.get_response('bundle', 'byobu/4/bat')
536 self.assertEqual(200, response.status_code)
537
538 def test_results_match(self):
539- self.maxDiff = None
540- bundle = self.makeBundle(name='bat', owner='bac', basket='byobu-4')
541- response = self.get_response('bundle', '~bac/byobu-4/bat')
542+ bundle = self.makeBundle(name='bat', owner='bac', basket='byobu/4')
543+ response = self.get_response('bundle', '~bac/byobu/4/bat')
544 self.assertEqual(200, response.status_code)
545- self.assertEqual(bundle.id, response.json['_id'])
546- # Copy the json dict so we can modify it.
547 json = dict(response.json)
548- del json['_id']
549- self.assertEqual(json, bundle._representation)
550+ self.assertEqual(json['bat'], bundle.data)
551
552
553 class TestAPI2Bundles(TestAPIBundles, API2Mixin):

Subscribers

People subscribed via source and target branches