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
1=== modified file 'lib/lp/registry/model/milestone.py'
2--- lib/lp/registry/model/milestone.py 2010-08-20 20:31:18 +0000
3+++ lib/lp/registry/model/milestone.py 2010-09-07 21:32:48 +0000
4@@ -36,6 +36,7 @@
5 sqlvalues,
6 )
7 from canonical.launchpad.webapp.sorting import expand_numbers
8+from lazr.restful.error import expose
9 from lp.app.errors import NotFoundError
10 from lp.blueprints.model.specification import Specification
11 from lp.bugs.interfaces.bugtarget import IHasBugs
12@@ -109,6 +110,13 @@
13 return sorted(result, key=milestone_sort_key, reverse=True)
14
15
16+class MultipleProductReleases(Exception):
17+ """Raised when a second ProductRelease is created for a milestone."""
18+
19+ def __init__(self, msg='A milestone can only have one ProductRelease.'):
20+ super(MultipleProductReleases, self).__init__(msg)
21+
22+
23 class Milestone(SQLBase, StructuralSubscriptionTargetMixin, HasBugsBase):
24 implements(IHasBugs, IMilestone)
25
26@@ -194,8 +202,7 @@
27 changelog=None, release_notes=None):
28 """See `IMilestone`."""
29 if self.product_release is not None:
30- raise AssertionError(
31- 'A milestone can only have one ProductRelease.')
32+ raise expose(MultipleProductReleases())
33 release = ProductRelease(
34 owner=owner,
35 changelog=changelog,
36
37=== modified file 'lib/lp/registry/model/product.py'
38--- lib/lp/registry/model/product.py 2010-09-03 11:05:21 +0000
39+++ lib/lp/registry/model/product.py 2010-09-07 21:32:48 +0000
40@@ -17,6 +17,7 @@
41 import operator
42
43 from lazr.delegates import delegates
44+from lazr.restful.error import expose
45 import pytz
46 from sqlobject import (
47 BoolCol,
48@@ -271,6 +272,13 @@
49 tables=[ProductLicense]))
50
51
52+class UnDeactivateable(Exception):
53+ """Raised when a project is requested to deactivate but can not."""
54+
55+ def __init__(self, msg):
56+ super(UnDeactivateable, self).__init__(msg)
57+
58+
59 class Product(SQLBase, BugTargetBase, MakesAnnouncements,
60 HasSpecificationsMixin, HasSprintsMixin,
61 KarmaContextMixin, BranchVisibilityPolicyMixin,
62@@ -440,9 +448,9 @@
63 # Validate deactivation.
64 if self.active == True and value == False:
65 if len(self.sourcepackages) > 0:
66- raise AssertionError(
67+ raise expose(UnDeactivateable(
68 'This project cannot be deactivated since it is '
69- 'linked to source packages.')
70+ 'linked to source packages.'))
71 return value
72
73 active = BoolCol(dbName='active', notNull=True, default=True,
74
75=== modified file 'lib/lp/registry/stories/webservice/xx-project-registry.txt'
76--- lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-07-20 17:50:45 +0000
77+++ lib/lp/registry/stories/webservice/xx-project-registry.txt 2010-09-07 21:32:48 +0000
78@@ -1016,9 +1016,9 @@
79 ... date_released='2000-01-01T01:01:01+00:00Z',
80 ... changelog='Added 5,000 features.')
81 >>> print response
82- HTTP/1.1 500 Internal Server Error
83+ HTTP/1.1 400 Bad Request
84 ...
85- AssertionError: A milestone can only have one ProductRelease.
86+ MultipleProductReleases: A milestone can only have one ProductRelease.
87
88
89 Project release entries
90
91=== modified file 'lib/lp/registry/tests/test_milestone.py'
92--- lib/lp/registry/tests/test_milestone.py 2010-08-20 20:31:18 +0000
93+++ lib/lp/registry/tests/test_milestone.py 2010-09-07 21:32:48 +0000
94@@ -15,6 +15,7 @@
95 logout,
96 )
97 from canonical.testing import LaunchpadFunctionalLayer
98+
99 from lp.app.errors import NotFoundError
100 from lp.registry.interfaces.distribution import IDistributionSet
101 from lp.registry.interfaces.milestone import IMilestoneSet
102@@ -79,6 +80,7 @@
103 all_visible_milestones_ids,
104 [1, 2, 3])
105
106+
107 def test_suite():
108 """Return the test suite for the tests in this module."""
109 return unittest.TestLoader().loadTestsFromName(__name__)
110
111=== modified file 'lib/lp/registry/tests/test_product.py'
112--- lib/lp/registry/tests/test_product.py 2010-08-25 20:04:40 +0000
113+++ lib/lp/registry/tests/test_product.py 2010-09-07 21:32:48 +0000
114@@ -28,7 +28,7 @@
115 License,
116 )
117 from lp.registry.model.commercialsubscription import CommercialSubscription
118-from lp.registry.model.product import Product
119+from lp.registry.model.product import Product, UnDeactivateable
120 from lp.registry.model.productlicense import ProductLicense
121 from lp.testing import TestCaseWithFactory
122
123@@ -48,7 +48,7 @@
124 source_package.setPackaging(
125 product.development_focus, self.factory.makePerson())
126 self.assertRaises(
127- AssertionError,
128+ UnDeactivateable,
129 setattr, product, 'active', False)
130
131 def test_deactivation_success(self):
132
133=== modified file 'lib/lp/registry/tests/test_project.py'
134--- lib/lp/registry/tests/test_project.py 2010-08-20 20:31:18 +0000
135+++ lib/lp/registry/tests/test_project.py 2010-09-07 21:32:48 +0000
136@@ -6,11 +6,22 @@
137 import unittest
138
139 from zope.component import getUtility
140+from lazr.restfulclient.errors import ClientError
141
142+from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
143 from canonical.launchpad.ftests import login
144-from canonical.testing import LaunchpadFunctionalLayer
145+from canonical.launchpad.webapp.errorlog import globalErrorUtility
146+from canonical.testing import (
147+ LaunchpadFunctionalLayer,
148+ DatabaseFunctionalLayer,
149+ )
150 from lp.registry.interfaces.projectgroup import IProjectGroupSet
151-from lp.testing import TestCaseWithFactory
152+from lp.soyuz.enums import ArchivePurpose
153+from lp.testing import (
154+ celebrity_logged_in,
155+ launchpadlib_for,
156+ TestCaseWithFactory,
157+ )
158
159
160 class ProjectGroupSearchTestCase(TestCaseWithFactory):
161@@ -99,5 +110,27 @@
162 self.assertEqual(self.project3, results[0])
163
164
165+class TestLaunchpadlibAPI(TestCaseWithFactory):
166+ layer = DatabaseFunctionalLayer
167+
168+ def test_inappropriate_deactivation_does_not_cause_an_OOPS(self):
169+ # Make sure a 400 error and not an OOPS is returned when a ValueError
170+ # is raised when trying to deactivate a project that has source
171+ # releases.
172+ last_oops = globalErrorUtility.getLastOopsReport()
173+ launchpad = launchpadlib_for("test", "salgado", "WRITE_PUBLIC")
174+
175+ project = launchpad.projects['evolution']
176+ project.active = False
177+ e = self.assertRaises(ClientError, project.lp_save)
178+
179+ # no OOPS was generated as a result of the exception
180+ self.assertNoNewOops(last_oops)
181+ self.assertEqual(400, e.response.status)
182+ self.assertIn(
183+ 'This project cannot be deactivated since it is linked to source '
184+ 'packages.', e.content)
185+
186+
187 def test_suite():
188 return unittest.TestLoader().loadTestsFromName(__name__)
189
190=== modified file 'lib/lp/registry/tests/test_project_milestone.py'
191--- lib/lp/registry/tests/test_project_milestone.py 2010-08-20 20:31:18 +0000
192+++ lib/lp/registry/tests/test_project_milestone.py 2010-09-07 21:32:48 +0000
193@@ -10,12 +10,19 @@
194
195 from storm.store import Store
196 from zope.component import getUtility
197+import pytz
198
199 from canonical.launchpad.ftests import (
200 login,
201 syncUpdate,
202 )
203-from canonical.testing import LaunchpadFunctionalLayer
204+from canonical.launchpad.webapp.errorlog import globalErrorUtility
205+from canonical.testing import (
206+ LaunchpadFunctionalLayer,
207+ DatabaseFunctionalLayer,
208+ )
209+from lazr.restfulclient.errors import ClientError
210+
211 from lp.blueprints.interfaces.specification import (
212 ISpecificationSet,
213 SpecificationDefinitionStatus,
214@@ -30,6 +37,11 @@
215 from lp.registry.interfaces.person import IPersonSet
216 from lp.registry.interfaces.product import IProductSet
217 from lp.registry.interfaces.projectgroup import IProjectGroupSet
218+from lp.registry.model.milestone import MultipleProductReleases
219+from lp.testing import (
220+ launchpadlib_for,
221+ TestCaseWithFactory,
222+ )
223
224
225 class ProjectMilestoneTest(unittest.TestCase):
226@@ -197,7 +209,7 @@
227 spec = specset.new(
228 name='%s-specification' % product_name,
229 title='Title %s specification' % product_name,
230- specurl='http://www.example.com/spec/%s' %product_name ,
231+ specurl='http://www.example.com/spec/%s' % product_name,
232 summary='summary',
233 definition_status=SpecificationDefinitionStatus.APPROVED,
234 priority=SpecificationPriority.HIGH,
235@@ -317,6 +329,48 @@
236 self._createProductSeriesBugtask('evolution', 'trunk', '1.1')
237
238
239+class TestDuplicateProductReleases(TestCaseWithFactory):
240+ layer = DatabaseFunctionalLayer
241+
242+ def test_inappropriate_release_raises(self):
243+ # A milestone that already has a ProductRelease can not be given
244+ # another one.
245+ login('foo.bar@canonical.com')
246+ product_set = getUtility(IProductSet)
247+ product = product_set['evolution']
248+ series = product.getSeries('trunk')
249+ milestone = series.newMilestone(name='1.1', dateexpected=None)
250+ now = datetime.now(pytz.UTC)
251+ milestone.createProductRelease(1, now)
252+ self.assertRaises(MultipleProductReleases,
253+ milestone.createProductRelease, 1, now)
254+ try:
255+ milestone.createProductRelease(1, now)
256+ except MultipleProductReleases, e:
257+ self.assert_(
258+ str(e), 'A milestone can only have one ProductRelease.')
259+
260+ def test_inappropriate_deactivation_does_not_cause_an_OOPS(self):
261+ # Make sure a 400 error and not an OOPS is returned when an exception
262+ # is raised when trying to create a product release when a milestone
263+ # already has one.
264+ last_oops = globalErrorUtility.getLastOopsReport()
265+ launchpad = launchpadlib_for("test", "salgado", "WRITE_PUBLIC")
266+
267+ project = launchpad.projects['evolution']
268+ milestone = project.getMilestone(name='2.1.6')
269+ now = datetime.now(pytz.UTC)
270+
271+ e = self.assertRaises(
272+ ClientError, milestone.createProductRelease, date_released=now)
273+
274+ # no OOPS was generated as a result of the exception
275+ self.assertNoNewOops(last_oops)
276+ self.assertEqual(400, e.response.status)
277+ self.assertIn(
278+ 'A milestone can only have one ProductRelease.', e.content)
279+
280+
281 def test_suite():
282 """Return the test suite for the tests in this module."""
283 return unittest.TestLoader().loadTestsFromName(__name__)
284
285=== modified file 'versions.cfg'
286--- versions.cfg 2010-09-06 18:23:01 +0000
287+++ versions.cfg 2010-09-07 21:32:48 +0000
288@@ -31,7 +31,7 @@
289 lazr.delegates = 1.2.0
290 lazr.enum = 1.1.2
291 lazr.lifecycle = 1.1
292-lazr.restful = 0.11.3
293+lazr.restful = 0.13.0
294 lazr.restfulclient = 0.10.0
295 lazr.smtptest = 1.1
296 lazr.testing = 0.1.1