Merge lp:~ricardokirkner/rnr-server/review-crud-api-with-macaroons into lp:rnr-server

Proposed by Ricardo Kirkner
Status: Merged
Approved by: Ricardo Kirkner
Approved revision: 334
Merged at revision: 318
Proposed branch: lp:~ricardokirkner/rnr-server/review-crud-api-with-macaroons
Merge into: lp:rnr-server
Diff against target: 611 lines (+172/-107)
4 files modified
src/clickreviews/api/urls.py (+3/-1)
src/clickreviews/tests/test_handlers.py (+160/-65)
src/reviewsapp/api/urls.py (+2/-3)
src/reviewsapp/tests/test_handlers.py (+7/-38)
To merge this branch: bzr merge lp:~ricardokirkner/rnr-server/review-crud-api-with-macaroons
Reviewer Review Type Date Requested Status
Fabián Ezequiel Gallina (community) Approve
Review via email: mp+294951@code.launchpad.net

Commit message

add macaroons support for review CRUD apis

To post a comment you must log in.
Revision history for this message
Fabián Ezequiel Gallina (fgallina) wrote :

Looking good, added few comments.

Revision history for this message
Ricardo Kirkner (ricardokirkner) :
332. By Ricardo Kirkner

use public names for helper methods

333. By Ricardo Kirkner

refactor to avoid duplication

334. By Ricardo Kirkner

assert changes on db when deleting a review

Revision history for this message
Fabián Ezequiel Gallina (fgallina) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/clickreviews/api/urls.py'
2--- src/clickreviews/api/urls.py 2014-11-21 20:27:01 +0000
3+++ src/clickreviews/api/urls.py 2016-05-20 13:59:57 +0000
4@@ -9,6 +9,7 @@
5 from piston.resource import Resource
6 from core.api.auth import (
7 DelegatedSSOAuthentication,
8+ MacaroonsAuthentication,
9 SSOOAuthAuthentication,
10 )
11 from core.api.decorators import gzip_content, vary_only_on_headers
12@@ -24,9 +25,10 @@
13
14
15 auth = SSOOAuthAuthentication(realm="Ratings and Reviews")
16+macaroons_auth = MacaroonsAuthentication(realm="Ratings and Reviews")
17
18 review_resource = CSRFExemptResource(handler=ClickReviewHandler,
19- authentication=auth)
20+ authentication=(macaroons_auth, auth))
21 review_resource.callmap['PATCH'] = 'patch'
22 review_resource = never_cache(review_resource)
23
24
25=== modified file 'src/clickreviews/tests/test_handlers.py'
26--- src/clickreviews/tests/test_handlers.py 2015-04-22 14:04:11 +0000
27+++ src/clickreviews/tests/test_handlers.py 2016-05-20 13:59:57 +0000
28@@ -12,6 +12,7 @@
29 from django.core.cache import cache
30 from django.core.serializers.json import DateTimeAwareJSONEncoder
31 from django.core.urlresolvers import reverse
32+from mock import patch
33
34 from clickreviews.api.urls import sca_delegated_sso_auth
35 from clickreviews.models import (
36@@ -23,10 +24,28 @@
37 )
38 from clickreviews.tests.factory import TestCaseWithFactory
39 from core.models import BaseModeration
40-from core.tests.helpers import SSOTestCaseMixin, assert_no_extra_queries_after
41-
42-
43-class ClickReviewsHandlerTestCase(TestCaseWithFactory):
44+from core.tests.doubles import SCADouble
45+from core.tests.helpers import (
46+ RequestsDoubleTestCaseMixin,
47+ SSOTestCaseMixin,
48+ assert_no_extra_queries_after,
49+)
50+
51+
52+class MacaroonsAuthenticatedTestCase(TestCaseWithFactory,
53+ RequestsDoubleTestCaseMixin):
54+
55+ def setUp(self):
56+ super(MacaroonsAuthenticatedTestCase, self).setUp()
57+
58+ self.patch_requests()
59+ self.sca_double = SCADouble(self.requests_double)
60+ p = patch('core.utilities.web_services', new=self.sca_double)
61+ p.start()
62+ self.addCleanup(p.stop)
63+
64+
65+class ClickReviewsHandlerTestCase(MacaroonsAuthenticatedTestCase):
66
67 def setUp(self):
68 super(ClickReviewsHandlerTestCase, self).setUp()
69@@ -58,36 +77,38 @@
70 'arch_tag': 'i386',
71 }
72
73- def _post_new_review(self, data, user=None, verify_result=True):
74+ def post_new_review(self, data, user=None, verify_result=True,
75+ auth='oauth'):
76 # Post a review as an authenticated user.
77 url = reverse('click-api-reviews')
78- if user is None:
79- user = self.factory.makeUser()
80- self.client.login(username=user.username, password='test')
81+ headers = {}
82+ if auth == 'macaroon':
83+ authorization = 'Macaroon root="%s", discharge="%s"' % (
84+ 'root-macaroon-data', 'discharge-macaroon-data')
85+ headers['HTTP_AUTHORIZATION'] = authorization
86+ else:
87+ if user is None:
88+ user = self.factory.makeUser()
89+ self.client.login(username=user.username, password='test')
90
91 response = self.client.post(
92- url, data=data, content_type='application/json')
93+ url, data=data, content_type='application/json', **headers)
94 return response
95
96- def test_create(self):
97- data = self.make_data(package_name='com.example.me.myapp',
98- rating=5)
99-
100- response = self._post_new_review(json.dumps(data))
101-
102+ def assert_review_created(self, response, expected):
103 # Piston uses 200, rather than 201.
104 self.assertEqual(httplib.OK, response.status_code)
105 self.assertEqual('application/json; charset=utf-8',
106 response['content-type'])
107 reviews = ClickPackageReview.objects.filter(
108- click_package__package_name='com.example.me.myapp')
109+ click_package__package_name=expected['package_name'])
110 self.assertEqual(1, reviews.count())
111 review = reviews[0]
112 response_dict = json.loads(response.content)
113 self.assertEqual({
114 'id': review.id,
115- 'package_name': 'com.example.me.myapp',
116- 'language': data['language'],
117+ 'package_name': expected['package_name'],
118+ 'language': expected['language'],
119 'date_created': DateTimeAwareJSONEncoder().encode(
120 review.date_created).strip('"'),
121 'rating': 5,
122@@ -102,10 +123,27 @@
123 'usefulness_favorable': 0,
124 }, response_dict)
125
126+ def test_create(self):
127+ data = self.make_data(package_name='com.example.me.myapp',
128+ rating=5)
129+
130+ response = self.post_new_review(json.dumps(data))
131+ self.assert_review_created(response, data)
132+
133+ def test_create_using_macaroon_auth(self):
134+ user = self.factory.makeUser()
135+ account = self.make_account_data(user)
136+ self.sca_double.set_verify_acl_response(account=account)
137+ data = self.make_data(package_name='com.example.me.myapp',
138+ rating=5)
139+ response = self.post_new_review(
140+ json.dumps(data), user=user, auth='macaroon')
141+ self.assert_review_created(response, data)
142+
143 def test_invalid_rating(self):
144 data = self.make_data(rating=6)
145
146- response = self._post_new_review(json.dumps(data))
147+ response = self.post_new_review(json.dumps(data))
148
149 self.assertEqual(httplib.BAD_REQUEST, response.status_code)
150
151@@ -116,7 +154,7 @@
152 data = valid_data.copy()
153 del data[key]
154
155- response = self._post_new_review(json.dumps(data))
156+ response = self.post_new_review(json.dumps(data))
157
158 self.assertEqual(httplib.BAD_REQUEST, response.status_code)
159
160@@ -127,13 +165,13 @@
161 click_package = ClickPackage.objects.create(
162 package_name='com.example.me.myapp')
163
164- self._post_new_review(json.dumps(data))
165+ self.post_new_review(json.dumps(data))
166
167 click_package = ClickPackage.objects.get(id=click_package.id)
168 self.assertEqual(1, click_package.ratings_total)
169 self.assertEqual(5, click_package.ratings_average)
170
171- def _get_reviews(self, data=None):
172+ def get_reviews(self, data=None):
173 url = reverse('click-api-reviews')
174 kwargs = {}
175 if data is not None:
176@@ -146,7 +184,7 @@
177 for _ in range(5):
178 self.factory.makeClickPackageReview(click_package=package)
179
180- json_result = self._get_reviews(data={
181+ json_result = self.get_reviews(data={
182 'package_name': package.package_name,
183 })
184
185@@ -163,7 +201,7 @@
186 version='0.1.1', date_created=datetime(
187 2014, 1, 15, 11, 13, 55, 123456, tzinfo=pytz.UTC))
188
189- json_result = self._get_reviews(data={
190+ json_result = self.get_reviews(data={
191 'package_name': package.package_name,
192 })
193 self.assertEqual(200, json_result.status_code)
194@@ -190,7 +228,7 @@
195 }, result_review)
196
197 def test_must_filter_by_packagename(self):
198- result = self._get_reviews()
199+ result = self.get_reviews()
200
201 self.assertEqual(400, result.status_code)
202 self.assertEqual(['package_name'],
203@@ -202,7 +240,7 @@
204 click_package=package,
205 deleted=True)
206
207- json_result = self._get_reviews(data={
208+ json_result = self.get_reviews(data={
209 'package_name': package.package_name,
210 })
211 self.assertEqual(200, json_result.status_code)
212@@ -213,7 +251,7 @@
213 package = self.factory.makeClickPackage()
214 self.factory.makeClickPackageReview(click_package=package, hide=True)
215
216- json_result = self._get_reviews(data={
217+ json_result = self.get_reviews(data={
218 'package_name': package.package_name,
219 })
220 self.assertEqual(200, json_result.status_code)
221@@ -240,7 +278,7 @@
222 reviews[2].id,
223 ]
224
225- json_result = self._get_reviews(data={
226+ json_result = self.get_reviews(data={
227 'package_name': package.package_name,
228 })
229 self.assertEqual(200, json_result.status_code)
230@@ -255,33 +293,63 @@
231 self.factory.makeClickPackageReview(click_package=package)
232 assert_no_extra_queries_after(
233 lambda: self.factory.makeClickPackageReview(click_package=package),
234- self._get_reviews, data={'package_name': package.package_name}
235+ self.get_reviews, data={'package_name': package.package_name}
236 )
237
238- def _update_review(self, review, data, user=None):
239+ def update_review(self, review, data, user=None, auth='oauth'):
240 # Post a review as an authenticated user.
241 url = reverse('click-api-reviews', args=(review.pk,))
242- if user is None:
243- user = review.reviewer
244- self.client.login(username=user.username, password='test')
245+ headers = {}
246+ if auth == 'macaroon':
247+ authorization = 'Macaroon root="%s", discharge="%s"' % (
248+ 'root-macaroon-data', 'discharge-macaroon-data')
249+ headers['HTTP_AUTHORIZATION'] = authorization
250+ else:
251+ if user is None:
252+ user = review.reviewer
253+ self.client.login(username=user.username, password='test')
254 response = self.client.put(
255- url, data=json.dumps(data), content_type='application/json')
256+ url, data=json.dumps(data), content_type='application/json',
257+ **headers)
258 return response
259
260+ def assert_review_updated(self, response, expected):
261+ self.assertEqual(response.status_code, httplib.OK)
262+ review = json.loads(response.content)
263+ for key, value in expected.items():
264+ self.assertEqual(review[key], value)
265+
266 def test_update_review(self):
267 review = self.factory.makeClickPackageReview()
268 data = {'summary': 'New summary',
269 'review_text': 'Review text',
270 'rating': 5}
271- response = self._update_review(review, data)
272- self.assertEqual(response.status_code, httplib.OK)
273- review = json.loads(response.content)
274- for key, value in data.items():
275- self.assertEqual(review[key], data[key])
276+ response = self.update_review(review, data)
277+ self.assert_review_updated(response, data)
278+
279+ def make_account_data(self, user):
280+ return {
281+ 'verified': True,
282+ 'openid': user.useropenid_set.first().claimed_id.split('/')[-1],
283+ 'email': user.email,
284+ 'displayname': user.username,
285+ }
286+
287+ def test_update_review_using_macaroon_auth(self):
288+ user = self.factory.makeUser()
289+ account = self.make_account_data(user)
290+ self.sca_double.set_verify_acl_response(account=account)
291+ review = self.factory.makeClickPackageReview(reviewer=user)
292+ data = {'summary': 'New summary',
293+ 'review_text': 'Review text',
294+ 'rating': 5}
295+ response = self.update_review(
296+ review, data, user=user, auth='macaroon')
297+ self.assert_review_updated(response, data)
298
299 def test_update_review_missing_data(self):
300 review = self.factory.makeClickPackageReview()
301- response = self._update_review(review, {})
302+ response = self.update_review(review, {})
303 self.assertEqual(response.status_code, httplib.BAD_REQUEST)
304 errors = json.loads(response.content)
305 expected = {'errors':
306@@ -293,45 +361,72 @@
307 def test_update_review_wrong_user(self):
308 review = self.factory.makeClickPackageReview()
309 user = self.factory.makeUser()
310- response = self._update_review(review, {}, user)
311+ response = self.update_review(review, {}, user)
312 self.assertEqual(response.status_code, httplib.NOT_FOUND)
313
314- def _delete_review(self, review, user=None):
315+ def delete_review(self, review, user=None, auth='oauth'):
316 # Delete a review as an authenticated user.
317 url = reverse('click-api-reviews', args=(review.pk,))
318- if user is None:
319- user = review.reviewer
320- self.client.login(username=user.username, password='test')
321- response = self.client.delete(url)
322+ headers = {}
323+ if auth == 'macaroon':
324+ authorization = 'Macaroon root="%s", discharge="%s"' % (
325+ 'root-macaroon-data', 'discharge-macaroon-data')
326+ headers['HTTP_AUTHORIZATION'] = authorization
327+ else:
328+ if user is None:
329+ user = review.reviewer
330+ self.client.login(username=user.username, password='test')
331+ response = self.client.delete(url, **headers)
332 return response
333
334+ def assert_review_deleted(self, response):
335+ self.assertEqual(response.status_code, httplib.OK)
336+ review = json.loads(response.content)
337+ db_review = ClickPackageReview.objects.get(id=review['id'])
338+ self.assertTrue(db_review.hide)
339+ self.assertIsNotNone(db_review.date_deleted)
340+ # XXX: ignore time differences at the microsecond level
341+ date_deleted = db_review.date_deleted
342+ parsed = datetime.strptime(
343+ review['date_deleted'], '%Y-%m-%dT%H:%M:%S.%fZ')
344+ self.assertEqual(
345+ date_deleted.replace(microsecond=0),
346+ parsed.replace(microsecond=0, tzinfo=date_deleted.tzinfo))
347+
348 def test_delete_review(self):
349 review = self.factory.makeClickPackageReview()
350- response = self._delete_review(review)
351- self.assertEqual(response.status_code, httplib.OK)
352- review = json.loads(response.content)
353+ response = self.delete_review(review)
354+ self.assert_review_deleted(response)
355+
356+ def test_delete_review_using_macaroon_auth(self):
357+ user = self.factory.makeUser()
358+ account = self.make_account_data(user)
359+ self.sca_double.set_verify_acl_response(account=account)
360+ review = self.factory.makeClickPackageReview(reviewer=user)
361+ response = self.delete_review(review, auth='macaroon')
362+ self.assert_review_deleted(response)
363
364 def test_delete_review_wrong_id(self):
365 review = self.factory.makeClickPackageReview()
366 ClickPackageReview.objects.all().delete()
367- response = self._delete_review(review)
368+ response = self.delete_review(review)
369 self.assertEqual(response.status_code, httplib.NOT_FOUND)
370
371 def test_delete_already_deleted(self):
372 review = self.factory.makeClickPackageReview()
373 review.delete()
374- response = self._delete_review(review)
375+ response = self.delete_review(review)
376 self.assertEqual(response.status_code, httplib.NOT_FOUND)
377
378 def test_delete_hidden_review(self):
379 review = self.factory.makeClickPackageReview(hide=True)
380- response = self._delete_review(review)
381+ response = self.delete_review(review)
382 self.assertEqual(response.status_code, httplib.NOT_FOUND)
383
384 def test_delete_review_wrong_user(self):
385 review = self.factory.makeClickPackageReview()
386 user = self.factory.makeUser()
387- response = self._delete_review(review, user=user)
388+ response = self.delete_review(review, user=user)
389 self.assertEqual(response.status_code, httplib.NOT_FOUND)
390
391
392@@ -368,7 +463,7 @@
393 cache._cache.clear()
394 cache._expire_info.clear()
395
396- def _request_stats(self, data=None, headers=None, **kwargs):
397+ def request_stats(self, data=None, headers=None, **kwargs):
398 url = reverse(
399 'click-api-reviews-stats', kwargs=kwargs)
400 if data is None:
401@@ -385,7 +480,7 @@
402 click_package=self.factory.makeClickPackage('package_bar'),
403 language='de')
404
405- response = self._request_stats()
406+ response = self.request_stats()
407 reviews = json.loads(response.content)
408
409 foo = foo_review.click_package
410@@ -405,7 +500,7 @@
411 click_package=self.factory.makeClickPackage('package_bar'),
412 language='de')
413
414- response = self._request_stats(package_name='package_foo')
415+ response = self.request_stats(package_name='package_foo')
416 reviews = json.loads(response.content)
417
418 foo = foo_review.click_package
419@@ -413,12 +508,12 @@
420 pk=foo.pk, package_name=foo.package_name))
421
422 def test_read_for_missing_package(self):
423- response = self._request_stats(package_name='not_found')
424+ response = self.request_stats(package_name='not_found')
425 self.assertEqual(response.status_code, 404)
426
427 def test_request_cached_with_extra_headers(self):
428 # Requests for server status are never cached.
429- response = self._request_stats()
430+ response = self.request_stats()
431
432 self.assertEqual(
433 'max-age={0}'.format(settings.CACHE_REVIEW_STATS_SECONDS),
434@@ -427,13 +522,13 @@
435 # A subsequent call will not hit the view.
436 handler = 'clickreviews.api.handlers.ClickReviewsStatsHandler.read'
437 with mock.patch(handler) as mock_read:
438- response = self._request_stats()
439+ response = self.request_stats()
440 self.assertFalse(mock_read.called)
441
442 def test_cannot_request_with_get_params(self):
443 # Requesting the un-cached response (by adding get params) is
444 # not allowed.
445- response = self._request_stats(data=dict(foo='bar'))
446+ response = self.request_stats(data=dict(foo='bar'))
447
448 self.assertEqual(400, response.status_code)
449
450@@ -447,7 +542,7 @@
451 click_package=self.factory.makeClickPackage('package_bar'),
452 language='de')
453
454- response = self._request_stats(
455+ response = self.request_stats(
456 headers={'HTTP_ACCEPT_ENCODING': 'gzip, deflate'})
457
458 fileobj = BytesIO()
459@@ -468,7 +563,7 @@
460 pk=bar.pk, package_name=bar.package_name)])
461
462 def test_vary_headers(self):
463- response = self._request_stats()
464+ response = self.request_stats()
465 self.assertEqual(200, response.status_code)
466 self.assertTrue(response.has_header('vary'))
467 vary = response['vary']
468@@ -692,7 +787,7 @@
469 self.addCleanup(p.stop)
470 self.patch_sso()
471
472- def _make_delegated_sso_signed_headers(self, url, data=None):
473+ def make_delegated_sso_signed_headers(self, url, data=None):
474 if data is None:
475 data = ''
476 url = 'http://testserver' + url
477@@ -705,7 +800,7 @@
478
479 def request_get(self, url, authenticated=True, **headers):
480 if authenticated:
481- headers.update(self._make_delegated_sso_signed_headers(url))
482+ headers.update(self.make_delegated_sso_signed_headers(url))
483 return self.client.get(url, **headers)
484
485 def request_patch(
486@@ -714,7 +809,7 @@
487 if data is None:
488 data = ''
489 if authenticated:
490- headers.update(self._make_delegated_sso_signed_headers(url, data))
491+ headers.update(self.make_delegated_sso_signed_headers(url, data))
492 return self.client.patch(
493 url, data=data, content_type=content_type, **headers)
494
495
496=== modified file 'src/reviewsapp/api/urls.py'
497--- src/reviewsapp/api/urls.py 2016-05-16 14:32:26 +0000
498+++ src/reviewsapp/api/urls.py 2016-05-20 13:59:57 +0000
499@@ -22,7 +22,7 @@
500 )
501
502 from piston.resource import Resource
503-from core.api.auth import MacaroonsAuthentication, SSOOAuthAuthentication
504+from core.api.auth import SSOOAuthAuthentication
505 from core.api.decorators import (
506 gzip_content,
507 vary_only_on_accept,
508@@ -44,7 +44,6 @@
509 )
510
511 auth = SSOOAuthAuthentication(realm="Ratings and Reviews")
512-macaroons_auth = MacaroonsAuthentication(realm="Ratings and Reviews")
513
514
515 # The server status resource should never be cached.
516@@ -101,7 +100,7 @@
517 # The following POST handlers have POST data and will not be cached.
518 # The .__name__ hack is needed to support Django 1.1
519 submit_review_resource = CSRFExemptResource(
520- handler=SubmitReviewHandler, authentication=(macaroons_auth, auth))
521+ handler=SubmitReviewHandler, authentication=auth)
522 modify_review_resource = CSRFExemptResource(handler=ModifyReviewHandler,
523 authentication=auth)
524 flag_review_resource = CSRFExemptResource(handler=FlagReviewHandler,
525
526=== modified file 'src/reviewsapp/tests/test_handlers.py'
527--- src/reviewsapp/tests/test_handlers.py 2016-05-16 14:32:26 +0000
528+++ src/reviewsapp/tests/test_handlers.py 2016-05-20 13:59:57 +0000
529@@ -56,11 +56,7 @@
530 from piston.emitters import Emitter
531 from piston.handler import typemapper
532
533-from core.tests.doubles import SCADouble
534-from core.tests.helpers import (
535- RequestsDoubleTestCaseMixin,
536- assert_no_extra_queries_after,
537-)
538+from core.tests.helpers import assert_no_extra_queries_after
539 from reviewsapp.api.handlers import (
540 Django13JSONEncoder,
541 Django13JSONEmitter,
542@@ -987,8 +983,7 @@
543 self.assertEqual(response['vary'], 'Accept')
544
545
546-class SubmitReviewHandlerTestCase(TestCaseWithFactory,
547- RequestsDoubleTestCaseMixin):
548+class SubmitReviewHandlerTestCase(TestCaseWithFactory):
549
550 def setUp(self):
551 super(SubmitReviewHandlerTestCase, self).setUp()
552@@ -1005,26 +1000,13 @@
553 }
554 self.factory.makeArch('i386')
555
556- self.patch_requests()
557- self.sca_double = SCADouble(self.requests_double)
558- p = patch('core.utilities.web_services', new=self.sca_double)
559- p.start()
560- self.addCleanup(p.stop)
561-
562- def _post_new_review(self, data, user=None, verify_result=True,
563- auth='oauth'):
564+ def _post_new_review(self, data, user=None, verify_result=True):
565 # Post a review as an authenticated user.
566 url = reverse('rnr-api-reviews')
567
568- headers = {}
569- if auth == 'macaroon':
570- authorization = 'Macaroon root="%s", discharge="%s"' % (
571- 'root-macaroon-data', 'discharge-macaroon-data')
572- headers['HTTP_AUTHORIZATION'] = authorization
573- else:
574- if user is None:
575- user = self.factory.makeUser()
576- self.client.login(username=user.username, password='test')
577+ if user is None:
578+ user = self.factory.makeUser()
579+ self.client.login(username=user.username, password='test')
580
581 # Ensure we don't hit the network for our tests.
582 is_authed_fn = ('core.api.auth.SSOOAuthAuthentication.'
583@@ -1040,8 +1022,7 @@
584 with patch(sca_verify_fn) as mock_sca_verify_method:
585 mock_sca_verify_method.return_value = verify_result
586 response = self.client.post(
587- url, data=data, content_type='application/json',
588- **headers)
589+ url, data=data, content_type='application/json')
590 return response
591
592 def test_bogus_data(self):
593@@ -1077,18 +1058,6 @@
594 response_dict = simplejson.loads(response.content)
595 self.assertEqual('inkscape', response_dict['package_name'])
596
597- def test_create_using_macaroon_auth(self):
598- self.sca_double.set_verify_acl_response(
599- is_verified=True, openid='oid1234',
600- email='user@example.com', displayname='user1234')
601- response = self._post_new_review(simplejson.dumps(self.required_data),
602- auth='macaroon')
603- self.assertEqual(httplib.OK, response.status_code)
604- self.assertEqual('application/json; charset=utf-8',
605- response['content-type'])
606- response_dict = simplejson.loads(response.content)
607- self.assertEqual('inkscape', response_dict['package_name'])
608-
609 def test_creates_repository_and_software_item(self):
610 # Submitting a review for a software item or repository of which
611 # we don't yet have a record will create those records (after

Subscribers

People subscribed via source and target branches