Merge lp:~benji/launchpad/bug-413174 into lp:launchpad

Proposed by Benji York
Status: Merged
Approved by: Benji York
Approved revision: no longer in the source branch.
Merged at revision: 11548
Proposed branch: lp:~benji/launchpad/bug-413174
Merge into: lp:launchpad
Diff against target: 296 lines (+117/-13)
8 files modified
lib/lp/registry/model/milestone.py (+9/-2)
lib/lp/registry/model/product.py (+10/-2)
lib/lp/registry/stories/webservice/xx-project-registry.txt (+2/-2)
lib/lp/registry/tests/test_milestone.py (+2/-0)
lib/lp/registry/tests/test_product.py (+2/-2)
lib/lp/registry/tests/test_project.py (+35/-2)
lib/lp/registry/tests/test_project_milestone.py (+56/-2)
versions.cfg (+1/-1)
To merge this branch: bzr merge lp:~benji/launchpad/bug-413174
Reviewer Review Type Date Requested Status
Leonard Richardson (community) Approve
Review via email: mp+34794@code.launchpad.net

Description of the change

Often times functions have preconditions that must be met for the
functions to operate correctly. When users interact with those
functions via the web interface the precondition checks are often done
by view code. When called via the web service those functions instead
generate exceptions when called without the preconditions being met.

Previously those exceptions would be recorded as an OOPS and propagated
to the client as HTTP 500 errors with unhelpful response bodies.

This branch fixes bug 413174 by taking advantage of the
lazr.restful.error.expose function that signals lazr.restful to generate
an HTTP 400 error signaling a client error and causes the web service to
generate a response with the error message from the raised exception.
This also means that these client-induced errors will no longer generate
an OOPS.

In the future similar OOPSes that arrise can be given the same
treatment.

The tests added to verify the nice-error-message, non-OOPS producing
effects of this branch are in the TestLaunchpadlibAPI test cases in
lib/lp/registry/tests/test_project.py and TestDuplicateProductReleases
in lib/lp/registry/tests/test_project_milestone.py and can be run with

    bin/test -t TestLaunchpadlibAPI -t TestDuplicateProductReleases

A small bit of whitespace lint was cleaned up in this branch.

To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

Refactor get_last_oops_id and (as recommended by james_w) use assertNoNewOops. Looks good otherwise.

review: Approve
Revision history for this message
Benji York (benji) wrote :

> Refactor get_last_oops_id and (as recommended by james_w) use
> assertNoNewOops. Looks good otherwise.

Done.

Thanks for the review.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/model/milestone.py'
--- lib/lp/registry/model/milestone.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/model/milestone.py 2010-09-07 21:32:48 +0000
@@ -36,6 +36,7 @@
36 sqlvalues,36 sqlvalues,
37 )37 )
38from canonical.launchpad.webapp.sorting import expand_numbers38from canonical.launchpad.webapp.sorting import expand_numbers
39from lazr.restful.error import expose
39from lp.app.errors import NotFoundError40from lp.app.errors import NotFoundError
40from lp.blueprints.model.specification import Specification41from lp.blueprints.model.specification import Specification
41from lp.bugs.interfaces.bugtarget import IHasBugs42from lp.bugs.interfaces.bugtarget import IHasBugs
@@ -109,6 +110,13 @@
109 return sorted(result, key=milestone_sort_key, reverse=True)110 return sorted(result, key=milestone_sort_key, reverse=True)
110111
111112
113class MultipleProductReleases(Exception):
114 """Raised when a second ProductRelease is created for a milestone."""
115
116 def __init__(self, msg='A milestone can only have one ProductRelease.'):
117 super(MultipleProductReleases, self).__init__(msg)
118
119
112class Milestone(SQLBase, StructuralSubscriptionTargetMixin, HasBugsBase):120class Milestone(SQLBase, StructuralSubscriptionTargetMixin, HasBugsBase):
113 implements(IHasBugs, IMilestone)121 implements(IHasBugs, IMilestone)
114122
@@ -194,8 +202,7 @@
194 changelog=None, release_notes=None):202 changelog=None, release_notes=None):
195 """See `IMilestone`."""203 """See `IMilestone`."""
196 if self.product_release is not None:204 if self.product_release is not None:
197 raise AssertionError(205 raise expose(MultipleProductReleases())
198 'A milestone can only have one ProductRelease.')
199 release = ProductRelease(206 release = ProductRelease(
200 owner=owner,207 owner=owner,
201 changelog=changelog,208 changelog=changelog,
202209
=== modified file 'lib/lp/registry/model/product.py'
--- lib/lp/registry/model/product.py 2010-09-03 11:05:21 +0000
+++ lib/lp/registry/model/product.py 2010-09-07 21:32:48 +0000
@@ -17,6 +17,7 @@
17import operator17import operator
1818
19from lazr.delegates import delegates19from lazr.delegates import delegates
20from lazr.restful.error import expose
20import pytz21import pytz
21from sqlobject import (22from sqlobject import (
22 BoolCol,23 BoolCol,
@@ -271,6 +272,13 @@
271 tables=[ProductLicense]))272 tables=[ProductLicense]))
272273
273274
275class UnDeactivateable(Exception):
276 """Raised when a project is requested to deactivate but can not."""
277
278 def __init__(self, msg):
279 super(UnDeactivateable, self).__init__(msg)
280
281
274class Product(SQLBase, BugTargetBase, MakesAnnouncements,282class Product(SQLBase, BugTargetBase, MakesAnnouncements,
275 HasSpecificationsMixin, HasSprintsMixin,283 HasSpecificationsMixin, HasSprintsMixin,
276 KarmaContextMixin, BranchVisibilityPolicyMixin,284 KarmaContextMixin, BranchVisibilityPolicyMixin,
@@ -440,9 +448,9 @@
440 # Validate deactivation.448 # Validate deactivation.
441 if self.active == True and value == False:449 if self.active == True and value == False:
442 if len(self.sourcepackages) > 0:450 if len(self.sourcepackages) > 0:
443 raise AssertionError(451 raise expose(UnDeactivateable(
444 'This project cannot be deactivated since it is '452 'This project cannot be deactivated since it is '
445 'linked to source packages.')453 'linked to source packages.'))
446 return value454 return value
447455
448 active = BoolCol(dbName='active', notNull=True, default=True,456 active = BoolCol(dbName='active', notNull=True, default=True,
449457
=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-07-20 17:50:45 +0000
+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-09-07 21:32:48 +0000
@@ -1016,9 +1016,9 @@
1016 ... date_released='2000-01-01T01:01:01+00:00Z',1016 ... date_released='2000-01-01T01:01:01+00:00Z',
1017 ... changelog='Added 5,000 features.')1017 ... changelog='Added 5,000 features.')
1018 >>> print response1018 >>> print response
1019 HTTP/1.1 500 Internal Server Error1019 HTTP/1.1 400 Bad Request
1020 ...1020 ...
1021 AssertionError: A milestone can only have one ProductRelease.1021 MultipleProductReleases: A milestone can only have one ProductRelease.
10221022
10231023
1024Project release entries1024Project release entries
10251025
=== modified file 'lib/lp/registry/tests/test_milestone.py'
--- lib/lp/registry/tests/test_milestone.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/tests/test_milestone.py 2010-09-07 21:32:48 +0000
@@ -15,6 +15,7 @@
15 logout,15 logout,
16 )16 )
17from canonical.testing import LaunchpadFunctionalLayer17from canonical.testing import LaunchpadFunctionalLayer
18
18from lp.app.errors import NotFoundError19from lp.app.errors import NotFoundError
19from lp.registry.interfaces.distribution import IDistributionSet20from lp.registry.interfaces.distribution import IDistributionSet
20from lp.registry.interfaces.milestone import IMilestoneSet21from lp.registry.interfaces.milestone import IMilestoneSet
@@ -79,6 +80,7 @@
79 all_visible_milestones_ids,80 all_visible_milestones_ids,
80 [1, 2, 3])81 [1, 2, 3])
8182
83
82def test_suite():84def test_suite():
83 """Return the test suite for the tests in this module."""85 """Return the test suite for the tests in this module."""
84 return unittest.TestLoader().loadTestsFromName(__name__)86 return unittest.TestLoader().loadTestsFromName(__name__)
8587
=== modified file 'lib/lp/registry/tests/test_product.py'
--- lib/lp/registry/tests/test_product.py 2010-08-25 20:04:40 +0000
+++ lib/lp/registry/tests/test_product.py 2010-09-07 21:32:48 +0000
@@ -28,7 +28,7 @@
28 License,28 License,
29 )29 )
30from lp.registry.model.commercialsubscription import CommercialSubscription30from lp.registry.model.commercialsubscription import CommercialSubscription
31from lp.registry.model.product import Product31from lp.registry.model.product import Product, UnDeactivateable
32from lp.registry.model.productlicense import ProductLicense32from lp.registry.model.productlicense import ProductLicense
33from lp.testing import TestCaseWithFactory33from lp.testing import TestCaseWithFactory
3434
@@ -48,7 +48,7 @@
48 source_package.setPackaging(48 source_package.setPackaging(
49 product.development_focus, self.factory.makePerson())49 product.development_focus, self.factory.makePerson())
50 self.assertRaises(50 self.assertRaises(
51 AssertionError,51 UnDeactivateable,
52 setattr, product, 'active', False)52 setattr, product, 'active', False)
5353
54 def test_deactivation_success(self):54 def test_deactivation_success(self):
5555
=== modified file 'lib/lp/registry/tests/test_project.py'
--- lib/lp/registry/tests/test_project.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/tests/test_project.py 2010-09-07 21:32:48 +0000
@@ -6,11 +6,22 @@
6import unittest6import unittest
77
8from zope.component import getUtility8from zope.component import getUtility
9from lazr.restfulclient.errors import ClientError
910
11from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
10from canonical.launchpad.ftests import login12from canonical.launchpad.ftests import login
11from canonical.testing import LaunchpadFunctionalLayer13from canonical.launchpad.webapp.errorlog import globalErrorUtility
14from canonical.testing import (
15 LaunchpadFunctionalLayer,
16 DatabaseFunctionalLayer,
17 )
12from lp.registry.interfaces.projectgroup import IProjectGroupSet18from lp.registry.interfaces.projectgroup import IProjectGroupSet
13from lp.testing import TestCaseWithFactory19from lp.soyuz.enums import ArchivePurpose
20from lp.testing import (
21 celebrity_logged_in,
22 launchpadlib_for,
23 TestCaseWithFactory,
24 )
1425
1526
16class ProjectGroupSearchTestCase(TestCaseWithFactory):27class ProjectGroupSearchTestCase(TestCaseWithFactory):
@@ -99,5 +110,27 @@
99 self.assertEqual(self.project3, results[0])110 self.assertEqual(self.project3, results[0])
100111
101112
113class TestLaunchpadlibAPI(TestCaseWithFactory):
114 layer = DatabaseFunctionalLayer
115
116 def test_inappropriate_deactivation_does_not_cause_an_OOPS(self):
117 # Make sure a 400 error and not an OOPS is returned when a ValueError
118 # is raised when trying to deactivate a project that has source
119 # releases.
120 last_oops = globalErrorUtility.getLastOopsReport()
121 launchpad = launchpadlib_for("test", "salgado", "WRITE_PUBLIC")
122
123 project = launchpad.projects['evolution']
124 project.active = False
125 e = self.assertRaises(ClientError, project.lp_save)
126
127 # no OOPS was generated as a result of the exception
128 self.assertNoNewOops(last_oops)
129 self.assertEqual(400, e.response.status)
130 self.assertIn(
131 'This project cannot be deactivated since it is linked to source '
132 'packages.', e.content)
133
134
102def test_suite():135def test_suite():
103 return unittest.TestLoader().loadTestsFromName(__name__)136 return unittest.TestLoader().loadTestsFromName(__name__)
104137
=== modified file 'lib/lp/registry/tests/test_project_milestone.py'
--- lib/lp/registry/tests/test_project_milestone.py 2010-08-20 20:31:18 +0000
+++ lib/lp/registry/tests/test_project_milestone.py 2010-09-07 21:32:48 +0000
@@ -10,12 +10,19 @@
1010
11from storm.store import Store11from storm.store import Store
12from zope.component import getUtility12from zope.component import getUtility
13import pytz
1314
14from canonical.launchpad.ftests import (15from canonical.launchpad.ftests import (
15 login,16 login,
16 syncUpdate,17 syncUpdate,
17 )18 )
18from canonical.testing import LaunchpadFunctionalLayer19from canonical.launchpad.webapp.errorlog import globalErrorUtility
20from canonical.testing import (
21 LaunchpadFunctionalLayer,
22 DatabaseFunctionalLayer,
23 )
24from lazr.restfulclient.errors import ClientError
25
19from lp.blueprints.interfaces.specification import (26from lp.blueprints.interfaces.specification import (
20 ISpecificationSet,27 ISpecificationSet,
21 SpecificationDefinitionStatus,28 SpecificationDefinitionStatus,
@@ -30,6 +37,11 @@
30from lp.registry.interfaces.person import IPersonSet37from lp.registry.interfaces.person import IPersonSet
31from lp.registry.interfaces.product import IProductSet38from lp.registry.interfaces.product import IProductSet
32from lp.registry.interfaces.projectgroup import IProjectGroupSet39from lp.registry.interfaces.projectgroup import IProjectGroupSet
40from lp.registry.model.milestone import MultipleProductReleases
41from lp.testing import (
42 launchpadlib_for,
43 TestCaseWithFactory,
44 )
3345
3446
35class ProjectMilestoneTest(unittest.TestCase):47class ProjectMilestoneTest(unittest.TestCase):
@@ -197,7 +209,7 @@
197 spec = specset.new(209 spec = specset.new(
198 name='%s-specification' % product_name,210 name='%s-specification' % product_name,
199 title='Title %s specification' % product_name,211 title='Title %s specification' % product_name,
200 specurl='http://www.example.com/spec/%s' %product_name ,212 specurl='http://www.example.com/spec/%s' % product_name,
201 summary='summary',213 summary='summary',
202 definition_status=SpecificationDefinitionStatus.APPROVED,214 definition_status=SpecificationDefinitionStatus.APPROVED,
203 priority=SpecificationPriority.HIGH,215 priority=SpecificationPriority.HIGH,
@@ -317,6 +329,48 @@
317 self._createProductSeriesBugtask('evolution', 'trunk', '1.1')329 self._createProductSeriesBugtask('evolution', 'trunk', '1.1')
318330
319331
332class TestDuplicateProductReleases(TestCaseWithFactory):
333 layer = DatabaseFunctionalLayer
334
335 def test_inappropriate_release_raises(self):
336 # A milestone that already has a ProductRelease can not be given
337 # another one.
338 login('foo.bar@canonical.com')
339 product_set = getUtility(IProductSet)
340 product = product_set['evolution']
341 series = product.getSeries('trunk')
342 milestone = series.newMilestone(name='1.1', dateexpected=None)
343 now = datetime.now(pytz.UTC)
344 milestone.createProductRelease(1, now)
345 self.assertRaises(MultipleProductReleases,
346 milestone.createProductRelease, 1, now)
347 try:
348 milestone.createProductRelease(1, now)
349 except MultipleProductReleases, e:
350 self.assert_(
351 str(e), 'A milestone can only have one ProductRelease.')
352
353 def test_inappropriate_deactivation_does_not_cause_an_OOPS(self):
354 # Make sure a 400 error and not an OOPS is returned when an exception
355 # is raised when trying to create a product release when a milestone
356 # already has one.
357 last_oops = globalErrorUtility.getLastOopsReport()
358 launchpad = launchpadlib_for("test", "salgado", "WRITE_PUBLIC")
359
360 project = launchpad.projects['evolution']
361 milestone = project.getMilestone(name='2.1.6')
362 now = datetime.now(pytz.UTC)
363
364 e = self.assertRaises(
365 ClientError, milestone.createProductRelease, date_released=now)
366
367 # no OOPS was generated as a result of the exception
368 self.assertNoNewOops(last_oops)
369 self.assertEqual(400, e.response.status)
370 self.assertIn(
371 'A milestone can only have one ProductRelease.', e.content)
372
373
320def test_suite():374def test_suite():
321 """Return the test suite for the tests in this module."""375 """Return the test suite for the tests in this module."""
322 return unittest.TestLoader().loadTestsFromName(__name__)376 return unittest.TestLoader().loadTestsFromName(__name__)
323377
=== modified file 'versions.cfg'
--- versions.cfg 2010-09-06 18:23:01 +0000
+++ versions.cfg 2010-09-07 21:32:48 +0000
@@ -31,7 +31,7 @@
31lazr.delegates = 1.2.031lazr.delegates = 1.2.0
32lazr.enum = 1.1.232lazr.enum = 1.1.2
33lazr.lifecycle = 1.133lazr.lifecycle = 1.1
34lazr.restful = 0.11.334lazr.restful = 0.13.0
35lazr.restfulclient = 0.10.035lazr.restfulclient = 0.10.0
36lazr.smtptest = 1.136lazr.smtptest = 1.1
37lazr.testing = 0.1.137lazr.testing = 0.1.1