Merge lp:~cjwatson/launchpad/snap-webservice into lp:launchpad
- snap-webservice
- Merge into devel
Proposed by
Colin Watson
Status: | Merged | ||||
---|---|---|---|---|---|
Merged at revision: | 17664 | ||||
Proposed branch: | lp:~cjwatson/launchpad/snap-webservice | ||||
Merge into: | lp:launchpad | ||||
Prerequisite: | lp:~cjwatson/launchpad/snap-builds | ||||
Diff against target: |
1550 lines (+826/-132) 16 files modified
lib/lp/app/browser/launchpad.py (+2/-0) lib/lp/app/browser/tests/test_webservice.py (+6/-0) lib/lp/buildmaster/browser/builder.py (+8/-0) lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py (+3/-12) lib/lp/snappy/browser/configure.zcml (+4/-0) lib/lp/snappy/configure.zcml (+2/-0) lib/lp/snappy/interfaces/snap.py (+116/-33) lib/lp/snappy/interfaces/snapbuild.py (+42/-21) lib/lp/snappy/interfaces/webservice.py (+41/-0) lib/lp/snappy/model/snap.py (+5/-5) lib/lp/snappy/tests/test_snap.py (+396/-1) lib/lp/snappy/tests/test_snapbuild.py (+179/-2) lib/lp/soyuz/browser/tests/test_livefsbuild.py (+5/-11) lib/lp/soyuz/tests/test_livefsbuild.py (+2/-23) lib/lp/soyuz/tests/test_packageupload.py (+3/-24) lib/lp/testing/__init__.py (+12/-0) |
||||
To merge this branch: | bzr merge lp:~cjwatson/launchpad/snap-webservice | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
William Grant | code | Approve | |
Review via email: mp+265700@code.launchpad.net |
Commit message
Export Snap and SnapBuild on the webservice.
Description of the change
Export Snap and SnapBuild on the webservice.
To post a comment you must log in.
Revision history for this message
William Grant (wgrant) : | # |
review:
Approve
(code)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'lib/lp/app/browser/launchpad.py' |
2 | --- lib/lp/app/browser/launchpad.py 2015-07-08 16:05:11 +0000 |
3 | +++ lib/lp/app/browser/launchpad.py 2015-08-03 13:17:37 +0000 |
4 | @@ -157,6 +157,7 @@ |
5 | from lp.services.webapp.url import urlappend |
6 | from lp.services.worlddata.interfaces.country import ICountrySet |
7 | from lp.services.worlddata.interfaces.language import ILanguageSet |
8 | +from lp.snappy.interfaces.snap import ISnapSet |
9 | from lp.soyuz.interfaces.archive import IArchiveSet |
10 | from lp.soyuz.interfaces.binarypackagename import IBinaryPackageNameSet |
11 | from lp.soyuz.interfaces.livefs import ILiveFSSet |
12 | @@ -797,6 +798,7 @@ |
13 | '+processors': IProcessorSet, |
14 | 'projects': IProductSet, |
15 | 'projectgroups': IProjectGroupSet, |
16 | + '+snaps': ISnapSet, |
17 | 'sourcepackagenames': ISourcePackageNameSet, |
18 | 'specs': ISpecificationSet, |
19 | 'sprints': ISprintSet, |
20 | |
21 | === modified file 'lib/lp/app/browser/tests/test_webservice.py' |
22 | --- lib/lp/app/browser/tests/test_webservice.py 2014-05-06 12:54:34 +0000 |
23 | +++ lib/lp/app/browser/tests/test_webservice.py 2015-08-03 13:17:37 +0000 |
24 | @@ -153,6 +153,12 @@ |
25 | object_type = 'questions' |
26 | |
27 | |
28 | +class TestMissingSnaps(BaseMissingObjectWebService, TestCaseWithFactory): |
29 | + """Test NotFound for webservice snaps requests.""" |
30 | + |
31 | + object_type = '+snaps' |
32 | + |
33 | + |
34 | class TestMissingTemporaryBlobs( |
35 | BaseMissingObjectWebService, TestCaseWithFactory): |
36 | """Test NotFound for webservice temporary_blobs requests.""" |
37 | |
38 | === modified file 'lib/lp/buildmaster/browser/builder.py' |
39 | --- lib/lp/buildmaster/browser/builder.py 2015-03-24 09:59:20 +0000 |
40 | +++ lib/lp/buildmaster/browser/builder.py 2015-08-03 13:17:37 +0000 |
41 | @@ -55,6 +55,7 @@ |
42 | ) |
43 | from lp.services.webapp.batching import StormRangeFactory |
44 | from lp.services.webapp.breadcrumb import Breadcrumb |
45 | +from lp.snappy.interfaces.snapbuild import ISnapBuildSet |
46 | from lp.soyuz.browser.build import ( |
47 | BuildRecordsView, |
48 | get_build_by_id_str, |
49 | @@ -88,6 +89,13 @@ |
50 | return None |
51 | return self.redirectSubTree(canonical_url(build)) |
52 | |
53 | + @stepthrough('+snapbuild') |
54 | + def traverse_snapbuild(self, name): |
55 | + build = get_build_by_id_str(ISnapBuildSet, name) |
56 | + if build is None: |
57 | + return None |
58 | + return self.redirectSubTree(canonical_url(build)) |
59 | + |
60 | |
61 | class BuilderSetBreadcrumb(Breadcrumb): |
62 | """Builds a breadcrumb for an `IBuilderSet`.""" |
63 | |
64 | === modified file 'lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py' |
65 | --- lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py 2015-04-20 15:59:52 +0000 |
66 | +++ lib/lp/code/browser/tests/test_sourcepackagerecipebuild.py 2015-08-03 13:17:37 +0000 |
67 | @@ -30,8 +30,6 @@ |
68 | extract_text, |
69 | find_main_content, |
70 | find_tags_by_class, |
71 | - setupBrowser, |
72 | - setupBrowserForUser, |
73 | ) |
74 | from lp.testing.sampledata import ADMIN_EMAIL |
75 | |
76 | @@ -276,17 +274,11 @@ |
77 | build.buildqueue_record.logtail = 'i am failing' |
78 | return build |
79 | |
80 | - def makeNonRedirectingBrowser(self, url, user=None): |
81 | - browser = setupBrowserForUser(user) if user else setupBrowser() |
82 | - browser.mech_browser.set_handle_equiv(False) |
83 | - browser.open(url) |
84 | - return browser |
85 | - |
86 | def test_builder_index_public(self): |
87 | build = self.makeBuildingRecipe() |
88 | url = canonical_url(build.builder) |
89 | logout() |
90 | - browser = self.makeNonRedirectingBrowser(url) |
91 | + browser = self.getNonRedirectingBrowser(url=url, user=ANONYMOUS) |
92 | self.assertIn('i am failing', browser.contents) |
93 | |
94 | def test_builder_index_private(self): |
95 | @@ -294,13 +286,12 @@ |
96 | with admin_logged_in(): |
97 | build = self.makeBuildingRecipe(archive=archive) |
98 | url = canonical_url(removeSecurityProxy(build).builder) |
99 | - random_person = self.factory.makePerson() |
100 | logout() |
101 | |
102 | # An unrelated user can't see the logtail of a private build. |
103 | - browser = self.makeNonRedirectingBrowser(url, random_person) |
104 | + browser = self.getNonRedirectingBrowser(url=url) |
105 | self.assertNotIn('i am failing', browser.contents) |
106 | |
107 | # But someone who can see the archive can. |
108 | - browser = self.makeNonRedirectingBrowser(url, archive.owner) |
109 | + browser = self.getNonRedirectingBrowser(url=url, user=archive.owner) |
110 | self.assertIn('i am failing', browser.contents) |
111 | |
112 | === modified file 'lib/lp/snappy/browser/configure.zcml' |
113 | --- lib/lp/snappy/browser/configure.zcml 2015-07-23 16:02:58 +0000 |
114 | +++ lib/lp/snappy/browser/configure.zcml 2015-08-03 13:17:37 +0000 |
115 | @@ -17,6 +17,10 @@ |
116 | module="lp.snappy.browser.snap" |
117 | classes="SnapNavigation" /> |
118 | <browser:url |
119 | + for="lp.snappy.interfaces.snap.ISnapSet" |
120 | + path_expression="string:+snaps" |
121 | + parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot" /> |
122 | + <browser:url |
123 | for="lp.snappy.interfaces.snapbuild.ISnapBuild" |
124 | path_expression="string:+build/${id}" |
125 | attribute_to_parent="snap" /> |
126 | |
127 | === modified file 'lib/lp/snappy/configure.zcml' |
128 | --- lib/lp/snappy/configure.zcml 2015-07-30 12:45:25 +0000 |
129 | +++ lib/lp/snappy/configure.zcml 2015-08-03 13:17:37 +0000 |
130 | @@ -71,4 +71,6 @@ |
131 | <allow interface="lp.snappy.interfaces.snapbuild.ISnapFile" /> |
132 | </class> |
133 | |
134 | + <webservice:register module="lp.snappy.interfaces.webservice" /> |
135 | + |
136 | </configure> |
137 | |
138 | === modified file 'lib/lp/snappy/interfaces/snap.py' |
139 | --- lib/lp/snappy/interfaces/snap.py 2015-08-03 10:43:56 +0000 |
140 | +++ lib/lp/snappy/interfaces/snap.py 2015-08-03 13:17:37 +0000 |
141 | @@ -21,19 +21,37 @@ |
142 | 'SnapNotOwner', |
143 | ] |
144 | |
145 | +import httplib |
146 | + |
147 | +from lazr.lifecycle.snapshot import doNotSnapshot |
148 | +from lazr.restful.declarations import ( |
149 | + call_with, |
150 | + collection_default_content, |
151 | + error_status, |
152 | + export_as_webservice_collection, |
153 | + export_as_webservice_entry, |
154 | + export_destructor_operation, |
155 | + export_factory_operation, |
156 | + export_read_operation, |
157 | + export_write_operation, |
158 | + exported, |
159 | + operation_for_version, |
160 | + operation_parameters, |
161 | + operation_returns_entry, |
162 | + REQUEST_USER, |
163 | + ) |
164 | from lazr.restful.fields import ( |
165 | CollectionField, |
166 | Reference, |
167 | ReferenceChoice, |
168 | ) |
169 | -from zope.interface import ( |
170 | - Attribute, |
171 | - Interface, |
172 | - ) |
173 | +from zope.interface import Interface |
174 | from zope.schema import ( |
175 | Bool, |
176 | + Choice, |
177 | Datetime, |
178 | Int, |
179 | + List, |
180 | Text, |
181 | TextLine, |
182 | ) |
183 | @@ -49,16 +67,21 @@ |
184 | from lp.code.interfaces.branch import IBranch |
185 | from lp.code.interfaces.gitrepository import IGitRepository |
186 | from lp.registry.interfaces.distroseries import IDistroSeries |
187 | +from lp.registry.interfaces.person import IPerson |
188 | +from lp.registry.interfaces.pocket import PackagePublishingPocket |
189 | from lp.registry.interfaces.role import IHasOwner |
190 | from lp.services.fields import ( |
191 | PersonChoice, |
192 | PublicPersonChoice, |
193 | ) |
194 | +from lp.soyuz.interfaces.archive import IArchive |
195 | +from lp.soyuz.interfaces.distroarchseries import IDistroArchSeries |
196 | |
197 | |
198 | SNAP_FEATURE_FLAG = u"snap.allow_new" |
199 | |
200 | |
201 | +@error_status(httplib.BAD_REQUEST) |
202 | class SnapBuildAlreadyPending(Exception): |
203 | """A build was requested when an identical build was already pending.""" |
204 | |
205 | @@ -67,6 +90,7 @@ |
206 | "An identical build of this snap package is already pending.") |
207 | |
208 | |
209 | +@error_status(httplib.FORBIDDEN) |
210 | class SnapBuildArchiveOwnerMismatch(Forbidden): |
211 | """Builds against private archives require that owners match. |
212 | |
213 | @@ -84,6 +108,7 @@ |
214 | "if the snap package owner and the archive owner are equal.") |
215 | |
216 | |
217 | +@error_status(httplib.BAD_REQUEST) |
218 | class SnapBuildDisallowedArchitecture(Exception): |
219 | """A build was requested for a disallowed architecture.""" |
220 | |
221 | @@ -93,6 +118,7 @@ |
222 | das.displayname) |
223 | |
224 | |
225 | +@error_status(httplib.UNAUTHORIZED) |
226 | class SnapFeatureDisabled(Unauthorized): |
227 | """Only certain users can create new snap-related objects.""" |
228 | |
229 | @@ -102,6 +128,7 @@ |
230 | "builds.") |
231 | |
232 | |
233 | +@error_status(httplib.BAD_REQUEST) |
234 | class DuplicateSnapName(Exception): |
235 | """Raised for snap packages with duplicate name/owner.""" |
236 | |
237 | @@ -110,6 +137,7 @@ |
238 | "There is already a snap package with the same name and owner.") |
239 | |
240 | |
241 | +@error_status(httplib.UNAUTHORIZED) |
242 | class SnapNotOwner(Unauthorized): |
243 | """The registrant/requester is not the owner or a member of its team.""" |
244 | |
245 | @@ -119,6 +147,7 @@ |
246 | _message_prefix = "No such snap package with this owner" |
247 | |
248 | |
249 | +@error_status(httplib.BAD_REQUEST) |
250 | class NoSourceForSnap(Exception): |
251 | """Snap packages must have a source (Bazaar branch or Git repository).""" |
252 | |
253 | @@ -128,6 +157,7 @@ |
254 | "repository.") |
255 | |
256 | |
257 | +@error_status(httplib.BAD_REQUEST) |
258 | class CannotDeleteSnap(Exception): |
259 | """This snap package cannot be deleted.""" |
260 | |
261 | @@ -137,14 +167,22 @@ |
262 | |
263 | id = Int(title=_("ID"), required=True, readonly=True) |
264 | |
265 | - date_created = Datetime( |
266 | - title=_("Date created"), required=True, readonly=True) |
267 | + date_created = exported(Datetime( |
268 | + title=_("Date created"), required=True, readonly=True)) |
269 | |
270 | - registrant = PublicPersonChoice( |
271 | + registrant = exported(PublicPersonChoice( |
272 | title=_("Registrant"), required=True, readonly=True, |
273 | vocabulary="ValidPersonOrTeam", |
274 | - description=_("The person who registered this snap package.")) |
275 | + description=_("The person who registered this snap package."))) |
276 | |
277 | + @call_with(requester=REQUEST_USER) |
278 | + @operation_parameters( |
279 | + archive=Reference(schema=IArchive), |
280 | + distro_arch_series=Reference(schema=IDistroArchSeries), |
281 | + pocket=Choice(vocabulary=PackagePublishingPocket)) |
282 | + # Really ISnapBuild, patched in _schema_circular_imports.py. |
283 | + @export_factory_operation(Interface, []) |
284 | + @operation_for_version("devel") |
285 | def requestBuild(requester, archive, distro_arch_series, pocket): |
286 | """Request that the snap package be built. |
287 | |
288 | @@ -155,16 +193,36 @@ |
289 | :return: `ISnapBuild`. |
290 | """ |
291 | |
292 | - builds = Attribute("All builds of this snap package.") |
293 | - |
294 | - completed_builds = Attribute("Completed builds of this snap package.") |
295 | - |
296 | - pending_builds = Attribute("Pending builds of this snap package.") |
297 | + builds = exported(doNotSnapshot(CollectionField( |
298 | + title=_("All builds of this snap package."), |
299 | + description=_( |
300 | + "All builds of this snap package, sorted in descending order " |
301 | + "of finishing (or starting if not completed successfully)."), |
302 | + # Really ISnapBuild, patched in _schema_circular_imports.py. |
303 | + value_type=Reference(schema=Interface), readonly=True))) |
304 | + |
305 | + completed_builds = exported(doNotSnapshot(CollectionField( |
306 | + title=_("Completed builds of this snap package."), |
307 | + description=_( |
308 | + "Completed builds of this snap package, sorted in descending " |
309 | + "order of finishing."), |
310 | + # Really ISnapBuild, patched in _schema_circular_imports.py. |
311 | + value_type=Reference(schema=Interface), readonly=True))) |
312 | + |
313 | + pending_builds = exported(doNotSnapshot(CollectionField( |
314 | + title=_("Pending builds of this snap package."), |
315 | + description=_( |
316 | + "Pending builds of this snap package, sorted in descending " |
317 | + "order of creation."), |
318 | + # Really ISnapBuild, patched in _schema_circular_imports.py. |
319 | + value_type=Reference(schema=Interface), readonly=True))) |
320 | |
321 | |
322 | class ISnapEdit(Interface): |
323 | """`ISnap` methods that require launchpad.Edit permission.""" |
324 | |
325 | + @export_destructor_operation() |
326 | + @operation_for_version("devel") |
327 | def destroySelf(): |
328 | """Delete this snap package, provided that it has no builds.""" |
329 | |
330 | @@ -174,48 +232,48 @@ |
331 | |
332 | These attributes need launchpad.View to see, and launchpad.Edit to change. |
333 | """ |
334 | - date_last_modified = Datetime( |
335 | - title=_("Date last modified"), required=True, readonly=True) |
336 | + date_last_modified = exported(Datetime( |
337 | + title=_("Date last modified"), required=True, readonly=True)) |
338 | |
339 | - owner = PersonChoice( |
340 | + owner = exported(PersonChoice( |
341 | title=_("Owner"), required=True, readonly=False, |
342 | vocabulary="AllUserTeamsParticipationPlusSelf", |
343 | - description=_("The owner of this snap package.")) |
344 | + description=_("The owner of this snap package."))) |
345 | |
346 | - distro_series = Reference( |
347 | + distro_series = exported(Reference( |
348 | IDistroSeries, title=_("Distro Series"), required=True, readonly=False, |
349 | description=_( |
350 | - "The series for which the snap package should be built.")) |
351 | + "The series for which the snap package should be built."))) |
352 | |
353 | - name = TextLine( |
354 | + name = exported(TextLine( |
355 | title=_("Name"), required=True, readonly=False, |
356 | constraint=name_validator, |
357 | - description=_("The name of the snap package.")) |
358 | + description=_("The name of the snap package."))) |
359 | |
360 | - description = Text( |
361 | + description = exported(Text( |
362 | title=_("Description"), required=False, readonly=False, |
363 | - description=_("A description of the snap package.")) |
364 | + description=_("A description of the snap package."))) |
365 | |
366 | - branch = ReferenceChoice( |
367 | + branch = exported(ReferenceChoice( |
368 | title=_("Bazaar branch"), schema=IBranch, vocabulary="Branch", |
369 | required=False, readonly=False, |
370 | description=_( |
371 | "A Bazaar branch containing a snapcraft.yaml recipe at the top " |
372 | - "level.")) |
373 | + "level."))) |
374 | |
375 | - git_repository = ReferenceChoice( |
376 | + git_repository = exported(ReferenceChoice( |
377 | title=_("Git repository"), |
378 | schema=IGitRepository, vocabulary="GitRepository", |
379 | required=False, readonly=False, |
380 | description=_( |
381 | "A Git repository with a branch containing a snapcraft.yaml " |
382 | - "recipe at the top level.")) |
383 | + "recipe at the top level."))) |
384 | |
385 | - git_path = TextLine( |
386 | + git_path = exported(TextLine( |
387 | title=_("Git branch path"), required=False, readonly=False, |
388 | description=_( |
389 | "The path of the Git branch containing a snapcraft.yaml recipe at " |
390 | - "the top level.")) |
391 | + "the top level."))) |
392 | |
393 | |
394 | class ISnapAdminAttributes(Interface): |
395 | @@ -223,21 +281,26 @@ |
396 | |
397 | These attributes need launchpad.View to see, and launchpad.Admin to change. |
398 | """ |
399 | - require_virtualized = Bool( |
400 | + require_virtualized = exported(Bool( |
401 | title=_("Require virtualized builders"), required=True, readonly=False, |
402 | - description=_("Only build this snap package on virtual builders.")) |
403 | + description=_("Only build this snap package on virtual builders."))) |
404 | |
405 | - processors = CollectionField( |
406 | + processors = exported(CollectionField( |
407 | title=_("Processors"), |
408 | description=_( |
409 | "The architectures for which the snap package should be built."), |
410 | value_type=Reference(schema=IProcessor), |
411 | - readonly=False) |
412 | + readonly=False)) |
413 | |
414 | |
415 | class ISnapAdmin(Interface): |
416 | """`ISnap` methods that require launchpad.Admin permission.""" |
417 | |
418 | + @operation_parameters( |
419 | + processors=List( |
420 | + value_type=Reference(schema=IProcessor), required=True)) |
421 | + @export_write_operation() |
422 | + @operation_for_version("devel") |
423 | def setProcessors(processors): |
424 | """Set the architectures for which the snap package should be built.""" |
425 | |
426 | @@ -247,10 +310,23 @@ |
427 | ISnapAdmin): |
428 | """A buildable snap package.""" |
429 | |
430 | + # XXX cjwatson 2015-07-17 bug=760849: "beta" is a lie to get WADL |
431 | + # generation working. Individual attributes must set their version to |
432 | + # "devel". |
433 | + export_as_webservice_entry(as_of="beta") |
434 | + |
435 | |
436 | class ISnapSet(Interface): |
437 | """A utility to create and access snap packages.""" |
438 | |
439 | + export_as_webservice_collection(ISnap) |
440 | + |
441 | + @call_with(registrant=REQUEST_USER) |
442 | + @export_factory_operation( |
443 | + ISnap, [ |
444 | + "owner", "distro_series", "name", "description", "branch", |
445 | + "git_repository", "git_path"]) |
446 | + @operation_for_version("devel") |
447 | def new(registrant, owner, distro_series, name, description=None, |
448 | branch=None, git_repository=None, git_path=None, |
449 | require_virtualized=True, processors=None, date_created=None): |
450 | @@ -259,12 +335,19 @@ |
451 | def exists(owner, name): |
452 | """Check to see if a matching snap exists.""" |
453 | |
454 | + @operation_parameters( |
455 | + owner=Reference(IPerson, title=_("Owner"), required=True), |
456 | + name=TextLine(title=_("Snap name"), required=True)) |
457 | + @operation_returns_entry(ISnap) |
458 | + @export_read_operation() |
459 | + @operation_for_version("devel") |
460 | def getByName(owner, name): |
461 | """Return the appropriate `ISnap` for the given objects.""" |
462 | |
463 | def findByPerson(owner): |
464 | """Return all snap packages with the given `owner`.""" |
465 | |
466 | + @collection_default_content() |
467 | def empty_list(): |
468 | """Return an empty collection of snap packages. |
469 | |
470 | |
471 | === modified file 'lib/lp/snappy/interfaces/snapbuild.py' |
472 | --- lib/lp/snappy/interfaces/snapbuild.py 2015-07-23 16:02:58 +0000 |
473 | +++ lib/lp/snappy/interfaces/snapbuild.py 2015-08-03 13:17:37 +0000 |
474 | @@ -11,11 +11,16 @@ |
475 | 'ISnapFile', |
476 | ] |
477 | |
478 | +from lazr.restful.declarations import ( |
479 | + export_as_webservice_entry, |
480 | + export_read_operation, |
481 | + export_write_operation, |
482 | + exported, |
483 | + operation_for_version, |
484 | + operation_parameters, |
485 | + ) |
486 | from lazr.restful.fields import Reference |
487 | -from zope.interface import ( |
488 | - Attribute, |
489 | - Interface, |
490 | - ) |
491 | +from zope.interface import Interface |
492 | from zope.schema import ( |
493 | Bool, |
494 | Choice, |
495 | @@ -37,7 +42,11 @@ |
496 | class ISnapFile(Interface): |
497 | """A file produced by a snap package build.""" |
498 | |
499 | - snapbuild = Attribute("The snap package build producing this file.") |
500 | + snapbuild = Reference( |
501 | + # Really ISnapBuild, patched in _schema_circular_imports.py. |
502 | + Interface, |
503 | + title=_("The snap package build producing this file."), |
504 | + required=True, readonly=True) |
505 | |
506 | libraryfile = Reference( |
507 | ILibraryFileAlias, title=_("The library file alias for this file."), |
508 | @@ -47,46 +56,46 @@ |
509 | class ISnapBuildView(IPackageBuild): |
510 | """`ISnapBuild` attributes that require launchpad.View permission.""" |
511 | |
512 | - requester = Reference( |
513 | + requester = exported(Reference( |
514 | IPerson, |
515 | title=_("The person who requested this build."), |
516 | - required=True, readonly=True) |
517 | + required=True, readonly=True)) |
518 | |
519 | - snap = Reference( |
520 | + snap = exported(Reference( |
521 | ISnap, |
522 | title=_("The snap package to build."), |
523 | - required=True, readonly=True) |
524 | + required=True, readonly=True)) |
525 | |
526 | - archive = Reference( |
527 | + archive = exported(Reference( |
528 | IArchive, |
529 | title=_("The archive from which to build the snap package."), |
530 | - required=True, readonly=True) |
531 | + required=True, readonly=True)) |
532 | |
533 | - distro_arch_series = Reference( |
534 | + distro_arch_series = exported(Reference( |
535 | IDistroArchSeries, |
536 | title=_("The series and architecture for which to build."), |
537 | - required=True, readonly=True) |
538 | + required=True, readonly=True)) |
539 | |
540 | - pocket = Choice( |
541 | + pocket = exported(Choice( |
542 | title=_("The pocket for which to build."), |
543 | - vocabulary=PackagePublishingPocket, required=True, readonly=True) |
544 | + vocabulary=PackagePublishingPocket, required=True, readonly=True)) |
545 | |
546 | virtualized = Bool( |
547 | title=_("If True, this build is virtualized."), readonly=True) |
548 | |
549 | - score = Int( |
550 | + score = exported(Int( |
551 | title=_("Score of the related build farm job (if any)."), |
552 | - required=False, readonly=True) |
553 | + required=False, readonly=True)) |
554 | |
555 | - can_be_rescored = Bool( |
556 | + can_be_rescored = exported(Bool( |
557 | title=_("Can be rescored"), |
558 | required=True, readonly=True, |
559 | - description=_("Whether this build record can be rescored manually.")) |
560 | + description=_("Whether this build record can be rescored manually."))) |
561 | |
562 | - can_be_cancelled = Bool( |
563 | + can_be_cancelled = exported(Bool( |
564 | title=_("Can be cancelled"), |
565 | required=True, readonly=True, |
566 | - description=_("Whether this build record can be cancelled.")) |
567 | + description=_("Whether this build record can be cancelled."))) |
568 | |
569 | def getFiles(): |
570 | """Retrieve the build's `ISnapFile` records. |
571 | @@ -111,6 +120,8 @@ |
572 | :return: The corresponding `ILibraryFileAlias`. |
573 | """ |
574 | |
575 | + @export_read_operation() |
576 | + @operation_for_version("devel") |
577 | def getFileUrls(): |
578 | """URLs for all the files produced by this build. |
579 | |
580 | @@ -127,6 +138,8 @@ |
581 | :return: An `ISnapFile`. |
582 | """ |
583 | |
584 | + @export_write_operation() |
585 | + @operation_for_version("devel") |
586 | def cancel(): |
587 | """Cancel the build if it is either pending or in progress. |
588 | |
589 | @@ -145,6 +158,9 @@ |
590 | class ISnapBuildAdmin(Interface): |
591 | """`ISnapBuild` attributes that require launchpad.Admin.""" |
592 | |
593 | + @operation_parameters(score=Int(title=_("Score"), required=True)) |
594 | + @export_write_operation() |
595 | + @operation_for_version("devel") |
596 | def rescore(score): |
597 | """Change the build's score.""" |
598 | |
599 | @@ -152,6 +168,11 @@ |
600 | class ISnapBuild(ISnapBuildView, ISnapBuildEdit, ISnapBuildAdmin): |
601 | """Build information for snap package builds.""" |
602 | |
603 | + # XXX cjwatson 2014-05-06 bug=760849: "beta" is a lie to get WADL |
604 | + # generation working. Individual attributes must set their version to |
605 | + # "devel". |
606 | + export_as_webservice_entry(as_of="beta") |
607 | + |
608 | |
609 | class ISnapBuildSet(ISpecificBuildFarmJobSource): |
610 | """Utility for `ISnapBuild`.""" |
611 | |
612 | === added file 'lib/lp/snappy/interfaces/webservice.py' |
613 | --- lib/lp/snappy/interfaces/webservice.py 1970-01-01 00:00:00 +0000 |
614 | +++ lib/lp/snappy/interfaces/webservice.py 2015-08-03 13:17:37 +0000 |
615 | @@ -0,0 +1,41 @@ |
616 | +# Copyright 2015 Canonical Ltd. This software is licensed under the |
617 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
618 | + |
619 | +"""All the interfaces that are exposed through the webservice. |
620 | + |
621 | +There is a declaration in ZCML somewhere that looks like: |
622 | + <webservice:register module="lp.snappy.interfaces.webservice" /> |
623 | + |
624 | +which tells `lazr.restful` that it should look for webservice exports here. |
625 | +""" |
626 | + |
627 | +__all__ = [ |
628 | + 'ISnap', |
629 | + 'ISnapBuild', |
630 | + 'ISnapSet', |
631 | + ] |
632 | + |
633 | +from lp.services.webservice.apihelpers import ( |
634 | + patch_collection_property, |
635 | + patch_entry_return_type, |
636 | + patch_reference_property, |
637 | + ) |
638 | +from lp.snappy.interfaces.snap import ( |
639 | + ISnap, |
640 | + ISnapSet, |
641 | + ISnapView, |
642 | + ) |
643 | +from lp.snappy.interfaces.snapbuild import ( |
644 | + ISnapBuild, |
645 | + ISnapFile, |
646 | + ) |
647 | + |
648 | + |
649 | +# ISnapFile |
650 | +patch_reference_property(ISnapFile, 'snapbuild', ISnapBuild) |
651 | + |
652 | +# ISnapView |
653 | +patch_entry_return_type(ISnapView, 'requestBuild', ISnapBuild) |
654 | +patch_collection_property(ISnapView, 'builds', ISnapBuild) |
655 | +patch_collection_property(ISnapView, 'completed_builds', ISnapBuild) |
656 | +patch_collection_property(ISnapView, 'pending_builds', ISnapBuild) |
657 | |
658 | === modified file 'lib/lp/snappy/model/snap.py' |
659 | --- lib/lp/snappy/model/snap.py 2015-08-03 10:43:56 +0000 |
660 | +++ lib/lp/snappy/model/snap.py 2015-08-03 13:17:37 +0000 |
661 | @@ -302,17 +302,17 @@ |
662 | require_virtualized=require_virtualized, date_created=date_created) |
663 | store.add(snap) |
664 | |
665 | + try: |
666 | + store.flush() |
667 | + except IntegrityError: |
668 | + raise DuplicateSnapName |
669 | + |
670 | if processors is None: |
671 | processors = [ |
672 | p for p in getUtility(IProcessorSet).getAll() |
673 | if p.build_by_default] |
674 | snap.setProcessors(processors) |
675 | |
676 | - try: |
677 | - store.flush() |
678 | - except IntegrityError: |
679 | - raise DuplicateSnapName |
680 | - |
681 | return snap |
682 | |
683 | def _getByName(self, owner, name): |
684 | |
685 | === modified file 'lib/lp/snappy/tests/test_snap.py' |
686 | --- lib/lp/snappy/tests/test_snap.py 2015-08-03 10:43:56 +0000 |
687 | +++ lib/lp/snappy/tests/test_snap.py 2015-08-03 13:17:37 +0000 |
688 | @@ -5,11 +5,15 @@ |
689 | |
690 | __metaclass__ = type |
691 | |
692 | -from datetime import datetime |
693 | +from datetime import ( |
694 | + datetime, |
695 | + timedelta, |
696 | + ) |
697 | |
698 | from lazr.lifecycle.event import ObjectModifiedEvent |
699 | import pytz |
700 | from storm.locals import Store |
701 | +from testtools.matchers import Equals |
702 | import transaction |
703 | from zope.component import getUtility |
704 | from zope.event import notify |
705 | @@ -22,13 +26,16 @@ |
706 | from lp.buildmaster.interfaces.buildqueue import IBuildQueue |
707 | from lp.buildmaster.interfaces.processor import IProcessorSet |
708 | from lp.buildmaster.model.buildqueue import BuildQueue |
709 | +from lp.registry.interfaces.distribution import IDistributionSet |
710 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
711 | from lp.services.database.constants import UTC_NOW |
712 | from lp.services.features.testing import FeatureFixture |
713 | +from lp.services.webapp.interfaces import OAuthPermission |
714 | from lp.snappy.interfaces.snap import ( |
715 | CannotDeleteSnap, |
716 | ISnap, |
717 | ISnapSet, |
718 | + ISnapView, |
719 | NoSourceForSnap, |
720 | SNAP_FEATURE_FLAG, |
721 | SnapBuildAlreadyPending, |
722 | @@ -38,13 +45,23 @@ |
723 | from lp.snappy.interfaces.snapbuild import ISnapBuild |
724 | from lp.testing import ( |
725 | admin_logged_in, |
726 | + ANONYMOUS, |
727 | + api_url, |
728 | + login, |
729 | + logout, |
730 | person_logged_in, |
731 | + StormStatementRecorder, |
732 | TestCaseWithFactory, |
733 | ) |
734 | from lp.testing.layers import ( |
735 | DatabaseFunctionalLayer, |
736 | LaunchpadZopelessLayer, |
737 | ) |
738 | +from lp.testing.matchers import ( |
739 | + DoesNotSnapshot, |
740 | + HasQueryCount, |
741 | + ) |
742 | +from lp.testing.pages import webservice_for_person |
743 | |
744 | |
745 | class TestSnapFeatureFlag(TestCaseWithFactory): |
746 | @@ -73,6 +90,12 @@ |
747 | with admin_logged_in(): |
748 | self.assertProvides(snap, ISnap) |
749 | |
750 | + def test_avoids_problematic_snapshots(self): |
751 | + self.assertThat( |
752 | + self.factory.makeSnap(), |
753 | + DoesNotSnapshot( |
754 | + ["builds", "completed_builds", "pending_builds"], ISnapView)) |
755 | + |
756 | def test_initial_date_last_modified(self): |
757 | # The initial value of date_last_modified is date_created. |
758 | snap = self.factory.makeSnap( |
759 | @@ -478,3 +501,375 @@ |
760 | self.unrestricted_procs + [self.arm], snap.processors) |
761 | snap.processors = [] |
762 | self.assertContentEqual([], snap.processors) |
763 | + |
764 | + |
765 | +class TestSnapWebservice(TestCaseWithFactory): |
766 | + |
767 | + layer = DatabaseFunctionalLayer |
768 | + |
769 | + def setUp(self): |
770 | + super(TestSnapWebservice, self).setUp() |
771 | + self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"})) |
772 | + self.person = self.factory.makePerson(displayname="Test Person") |
773 | + self.webservice = webservice_for_person( |
774 | + self.person, permission=OAuthPermission.WRITE_PUBLIC) |
775 | + self.webservice.default_api_version = "devel" |
776 | + login(ANONYMOUS) |
777 | + |
778 | + def getURL(self, obj): |
779 | + return self.webservice.getAbsoluteUrl(api_url(obj)) |
780 | + |
781 | + def makeSnap(self, owner=None, distroseries=None, branch=None, |
782 | + git_ref=None, processors=None, webservice=None): |
783 | + if owner is None: |
784 | + owner = self.person |
785 | + if distroseries is None: |
786 | + distroseries = self.factory.makeDistroSeries(registrant=owner) |
787 | + if branch is None and git_ref is None: |
788 | + branch = self.factory.makeAnyBranch() |
789 | + kwargs = {} |
790 | + if webservice is None: |
791 | + webservice = self.webservice |
792 | + transaction.commit() |
793 | + distroseries_url = api_url(distroseries) |
794 | + owner_url = api_url(owner) |
795 | + if branch is not None: |
796 | + kwargs["branch"] = api_url(branch) |
797 | + if git_ref is not None: |
798 | + kwargs["git_repository"] = api_url(git_ref.repository) |
799 | + kwargs["git_path"] = git_ref.path |
800 | + if processors is not None: |
801 | + kwargs["processors"] = [ |
802 | + api_url(processor) for processor in processors] |
803 | + logout() |
804 | + response = webservice.named_post( |
805 | + "/+snaps", "new", owner=owner_url, distro_series=distroseries_url, |
806 | + name="mir", **kwargs) |
807 | + self.assertEqual(201, response.status) |
808 | + return webservice.get(response.getHeader("Location")).jsonBody() |
809 | + |
810 | + def getCollectionLinks(self, entry, member): |
811 | + """Return a list of self_link attributes of entries in a collection.""" |
812 | + collection = self.webservice.get( |
813 | + entry["%s_collection_link" % member]).jsonBody() |
814 | + return [entry["self_link"] for entry in collection["entries"]] |
815 | + |
816 | + def test_new_bzr(self): |
817 | + # Ensure Snap creation based on a Bazaar branch works. |
818 | + team = self.factory.makeTeam(owner=self.person) |
819 | + distroseries = self.factory.makeDistroSeries(registrant=team) |
820 | + branch = self.factory.makeAnyBranch() |
821 | + snap = self.makeSnap( |
822 | + owner=team, distroseries=distroseries, branch=branch) |
823 | + with person_logged_in(self.person): |
824 | + self.assertEqual(self.getURL(self.person), snap["registrant_link"]) |
825 | + self.assertEqual(self.getURL(team), snap["owner_link"]) |
826 | + self.assertEqual( |
827 | + self.getURL(distroseries), snap["distro_series_link"]) |
828 | + self.assertEqual("mir", snap["name"]) |
829 | + self.assertEqual(self.getURL(branch), snap["branch_link"]) |
830 | + self.assertIsNone(snap["git_repository_link"]) |
831 | + self.assertIsNone(snap["git_path"]) |
832 | + self.assertTrue(snap["require_virtualized"]) |
833 | + |
834 | + def test_new_git(self): |
835 | + # Ensure Snap creation based on a Git branch works. |
836 | + team = self.factory.makeTeam(owner=self.person) |
837 | + distroseries = self.factory.makeDistroSeries(registrant=team) |
838 | + [ref] = self.factory.makeGitRefs() |
839 | + snap = self.makeSnap( |
840 | + owner=team, distroseries=distroseries, git_ref=ref) |
841 | + with person_logged_in(self.person): |
842 | + self.assertEqual(self.getURL(self.person), snap["registrant_link"]) |
843 | + self.assertEqual(self.getURL(team), snap["owner_link"]) |
844 | + self.assertEqual( |
845 | + self.getURL(distroseries), snap["distro_series_link"]) |
846 | + self.assertEqual("mir", snap["name"]) |
847 | + self.assertIsNone(snap["branch_link"]) |
848 | + self.assertEqual( |
849 | + self.getURL(ref.repository), snap["git_repository_link"]) |
850 | + self.assertEqual(ref.path, snap["git_path"]) |
851 | + self.assertTrue(snap["require_virtualized"]) |
852 | + |
853 | + def test_duplicate(self): |
854 | + # An attempt to create a duplicate Snap fails. |
855 | + team = self.factory.makeTeam(owner=self.person) |
856 | + branch = self.factory.makeAnyBranch() |
857 | + branch_url = api_url(branch) |
858 | + self.makeSnap(owner=team) |
859 | + with person_logged_in(self.person): |
860 | + owner_url = api_url(team) |
861 | + distroseries_url = api_url(self.factory.makeDistroSeries()) |
862 | + response = self.webservice.named_post( |
863 | + "/+snaps", "new", owner=owner_url, distro_series=distroseries_url, |
864 | + name="mir", branch=branch_url) |
865 | + self.assertEqual(400, response.status) |
866 | + self.assertEqual( |
867 | + "There is already a snap package with the same name and owner.", |
868 | + response.body) |
869 | + |
870 | + def test_not_owner(self): |
871 | + # If the registrant is not the owner or a member of the owner team, |
872 | + # Snap creation fails. |
873 | + other_person = self.factory.makePerson(displayname="Other Person") |
874 | + other_team = self.factory.makeTeam( |
875 | + owner=other_person, displayname="Other Team") |
876 | + distroseries = self.factory.makeDistroSeries(registrant=self.person) |
877 | + branch = self.factory.makeAnyBranch() |
878 | + transaction.commit() |
879 | + other_person_url = api_url(other_person) |
880 | + other_team_url = api_url(other_team) |
881 | + distroseries_url = api_url(distroseries) |
882 | + branch_url = api_url(branch) |
883 | + logout() |
884 | + response = self.webservice.named_post( |
885 | + "/+snaps", "new", owner=other_person_url, |
886 | + distro_series=distroseries_url, name="dummy", branch=branch_url) |
887 | + self.assertEqual(401, response.status) |
888 | + self.assertEqual( |
889 | + "Test Person cannot create snap packages owned by Other Person.", |
890 | + response.body) |
891 | + response = self.webservice.named_post( |
892 | + "/+snaps", "new", owner=other_team_url, |
893 | + distro_series=distroseries_url, name="dummy", branch=branch_url) |
894 | + self.assertEqual(401, response.status) |
895 | + self.assertEqual( |
896 | + "Test Person is not a member of Other Team.", response.body) |
897 | + |
898 | + def test_getByName(self): |
899 | + # lp.snaps.getByName returns a matching Snap. |
900 | + snap = self.makeSnap() |
901 | + with person_logged_in(self.person): |
902 | + owner_url = api_url(self.person) |
903 | + response = self.webservice.named_get( |
904 | + "/+snaps", "getByName", owner=owner_url, name="mir") |
905 | + self.assertEqual(200, response.status) |
906 | + self.assertEqual(snap, response.jsonBody()) |
907 | + |
908 | + def test_getByName_missing(self): |
909 | + # lp.snaps.getByName returns 404 for a non-existent Snap. |
910 | + logout() |
911 | + with person_logged_in(self.person): |
912 | + owner_url = api_url(self.person) |
913 | + response = self.webservice.named_get( |
914 | + "/+snaps", "getByName", owner=owner_url, name="nonexistent") |
915 | + self.assertEqual(404, response.status) |
916 | + self.assertEqual( |
917 | + "No such snap package with this owner: 'nonexistent'.", |
918 | + response.body) |
919 | + |
920 | + def makeBuildableDistroArchSeries(self, **kwargs): |
921 | + das = self.factory.makeDistroArchSeries(**kwargs) |
922 | + fake_chroot = self.factory.makeLibraryFileAlias( |
923 | + filename="fake_chroot.tar.gz", db_only=True) |
924 | + das.addOrUpdateChroot(fake_chroot) |
925 | + return das |
926 | + |
927 | + def test_requestBuild(self): |
928 | + # Build requests can be performed and end up in snap.builds and |
929 | + # snap.pending_builds. |
930 | + distroseries = self.factory.makeDistroSeries(registrant=self.person) |
931 | + processor = self.factory.makeProcessor(supports_virtualized=True) |
932 | + distroarchseries = self.makeBuildableDistroArchSeries( |
933 | + distroseries=distroseries, processor=processor, owner=self.person) |
934 | + distroarchseries_url = api_url(distroarchseries) |
935 | + archive_url = api_url(distroseries.main_archive) |
936 | + snap = self.makeSnap(distroseries=distroseries, processors=[processor]) |
937 | + response = self.webservice.named_post( |
938 | + snap["self_link"], "requestBuild", archive=archive_url, |
939 | + distro_arch_series=distroarchseries_url, pocket="Release") |
940 | + self.assertEqual(201, response.status) |
941 | + build = self.webservice.get(response.getHeader("Location")).jsonBody() |
942 | + self.assertEqual( |
943 | + [build["self_link"]], self.getCollectionLinks(snap, "builds")) |
944 | + self.assertEqual([], self.getCollectionLinks(snap, "completed_builds")) |
945 | + self.assertEqual( |
946 | + [build["self_link"]], |
947 | + self.getCollectionLinks(snap, "pending_builds")) |
948 | + |
949 | + def test_requestBuild_rejects_repeats(self): |
950 | + # Build requests are rejected if already pending. |
951 | + distroseries = self.factory.makeDistroSeries(registrant=self.person) |
952 | + processor = self.factory.makeProcessor(supports_virtualized=True) |
953 | + distroarchseries = self.makeBuildableDistroArchSeries( |
954 | + distroseries=distroseries, processor=processor, owner=self.person) |
955 | + distroarchseries_url = api_url(distroarchseries) |
956 | + archive_url = api_url(distroseries.main_archive) |
957 | + snap = self.makeSnap(distroseries=distroseries, processors=[processor]) |
958 | + response = self.webservice.named_post( |
959 | + snap["self_link"], "requestBuild", archive=archive_url, |
960 | + distro_arch_series=distroarchseries_url, pocket="Release") |
961 | + self.assertEqual(201, response.status) |
962 | + response = self.webservice.named_post( |
963 | + snap["self_link"], "requestBuild", archive=archive_url, |
964 | + distro_arch_series=distroarchseries_url, pocket="Release") |
965 | + self.assertEqual(400, response.status) |
966 | + self.assertEqual( |
967 | + "An identical build of this snap package is already pending.", |
968 | + response.body) |
969 | + |
970 | + def test_requestBuild_not_owner(self): |
971 | + # If the requester is not the owner or a member of the owner team, |
972 | + # build requests are rejected. |
973 | + other_team = self.factory.makeTeam(displayname="Other Team") |
974 | + distroseries = self.factory.makeDistroSeries(registrant=self.person) |
975 | + processor = self.factory.makeProcessor(supports_virtualized=True) |
976 | + distroarchseries = self.makeBuildableDistroArchSeries( |
977 | + distroseries=distroseries, processor=processor, owner=self.person) |
978 | + distroarchseries_url = api_url(distroarchseries) |
979 | + archive_url = api_url(distroseries.main_archive) |
980 | + other_webservice = webservice_for_person( |
981 | + other_team.teamowner, permission=OAuthPermission.WRITE_PUBLIC) |
982 | + other_webservice.default_api_version = "devel" |
983 | + login(ANONYMOUS) |
984 | + snap = self.makeSnap( |
985 | + owner=other_team, distroseries=distroseries, |
986 | + processors=[processor], webservice=other_webservice) |
987 | + response = self.webservice.named_post( |
988 | + snap["self_link"], "requestBuild", archive=archive_url, |
989 | + distro_arch_series=distroarchseries_url, pocket="Release") |
990 | + self.assertEqual(401, response.status) |
991 | + self.assertEqual( |
992 | + "Test Person cannot create snap package builds owned by Other " |
993 | + "Team.", response.body) |
994 | + |
995 | + def test_requestBuild_archive_disabled(self): |
996 | + # Build requests against a disabled archive are rejected. |
997 | + distroseries = self.factory.makeDistroSeries( |
998 | + distribution=getUtility(IDistributionSet)['ubuntu'], |
999 | + registrant=self.person) |
1000 | + processor = self.factory.makeProcessor(supports_virtualized=True) |
1001 | + distroarchseries = self.makeBuildableDistroArchSeries( |
1002 | + distroseries=distroseries, processor=processor, owner=self.person) |
1003 | + distroarchseries_url = api_url(distroarchseries) |
1004 | + archive = self.factory.makeArchive( |
1005 | + distribution=distroseries.distribution, owner=self.person, |
1006 | + enabled=False, displayname="Disabled Archive") |
1007 | + archive_url = api_url(archive) |
1008 | + snap = self.makeSnap(distroseries=distroseries, processors=[processor]) |
1009 | + response = self.webservice.named_post( |
1010 | + snap["self_link"], "requestBuild", archive=archive_url, |
1011 | + distro_arch_series=distroarchseries_url, pocket="Release") |
1012 | + self.assertEqual(403, response.status) |
1013 | + self.assertEqual("Disabled Archive is disabled.", response.body) |
1014 | + |
1015 | + def test_requestBuild_archive_private_owners_match(self): |
1016 | + # Build requests against a private archive are allowed if the Snap |
1017 | + # and Archive owners match exactly. |
1018 | + distroseries = self.factory.makeDistroSeries( |
1019 | + distribution=getUtility(IDistributionSet)['ubuntu'], |
1020 | + registrant=self.person) |
1021 | + processor = self.factory.makeProcessor(supports_virtualized=True) |
1022 | + distroarchseries = self.makeBuildableDistroArchSeries( |
1023 | + distroseries=distroseries, processor=processor, owner=self.person) |
1024 | + distroarchseries_url = api_url(distroarchseries) |
1025 | + archive = self.factory.makeArchive( |
1026 | + distribution=distroseries.distribution, owner=self.person, |
1027 | + private=True) |
1028 | + archive_url = api_url(archive) |
1029 | + snap = self.makeSnap(distroseries=distroseries, processors=[processor]) |
1030 | + response = self.webservice.named_post( |
1031 | + snap["self_link"], "requestBuild", archive=archive_url, |
1032 | + distro_arch_series=distroarchseries_url, pocket="Release") |
1033 | + self.assertEqual(201, response.status) |
1034 | + |
1035 | + def test_requestBuild_archive_private_owners_mismatch(self): |
1036 | + # Build requests against a private archive are rejected if the Snap |
1037 | + # and Archive owners do not match exactly. |
1038 | + distroseries = self.factory.makeDistroSeries( |
1039 | + distribution=getUtility(IDistributionSet)['ubuntu'], |
1040 | + registrant=self.person) |
1041 | + processor = self.factory.makeProcessor(supports_virtualized=True) |
1042 | + distroarchseries = self.makeBuildableDistroArchSeries( |
1043 | + distroseries=distroseries, processor=processor, owner=self.person) |
1044 | + distroarchseries_url = api_url(distroarchseries) |
1045 | + archive = self.factory.makeArchive( |
1046 | + distribution=distroseries.distribution, private=True) |
1047 | + archive_url = api_url(archive) |
1048 | + snap = self.makeSnap(distroseries=distroseries, processors=[processor]) |
1049 | + response = self.webservice.named_post( |
1050 | + snap["self_link"], "requestBuild", archive=archive_url, |
1051 | + distro_arch_series=distroarchseries_url, pocket="Release") |
1052 | + self.assertEqual(403, response.status) |
1053 | + self.assertEqual( |
1054 | + "Snap package builds against private archives are only allowed " |
1055 | + "if the snap package owner and the archive owner are equal.", |
1056 | + response.body) |
1057 | + |
1058 | + def test_getBuilds(self): |
1059 | + # The builds, completed_builds, and pending_builds properties are as |
1060 | + # expected. |
1061 | + distroseries = self.factory.makeDistroSeries( |
1062 | + distribution=getUtility(IDistributionSet)['ubuntu'], |
1063 | + registrant=self.person) |
1064 | + processor = self.factory.makeProcessor(supports_virtualized=True) |
1065 | + distroarchseries = self.makeBuildableDistroArchSeries( |
1066 | + distroseries=distroseries, processor=processor, owner=self.person) |
1067 | + distroarchseries_url = api_url(distroarchseries) |
1068 | + archives = [ |
1069 | + self.factory.makeArchive( |
1070 | + distribution=distroseries.distribution, owner=self.person) |
1071 | + for x in range(4)] |
1072 | + archive_urls = [api_url(archive) for archive in archives] |
1073 | + snap = self.makeSnap(distroseries=distroseries, processors=[processor]) |
1074 | + builds = [] |
1075 | + for archive_url in archive_urls: |
1076 | + response = self.webservice.named_post( |
1077 | + snap["self_link"], "requestBuild", archive=archive_url, |
1078 | + distro_arch_series=distroarchseries_url, pocket="Proposed") |
1079 | + self.assertEqual(201, response.status) |
1080 | + build = self.webservice.get( |
1081 | + response.getHeader("Location")).jsonBody() |
1082 | + builds.insert(0, build["self_link"]) |
1083 | + self.assertEqual(builds, self.getCollectionLinks(snap, "builds")) |
1084 | + self.assertEqual([], self.getCollectionLinks(snap, "completed_builds")) |
1085 | + self.assertEqual( |
1086 | + builds, self.getCollectionLinks(snap, "pending_builds")) |
1087 | + snap = self.webservice.get(snap["self_link"]).jsonBody() |
1088 | + |
1089 | + with person_logged_in(self.person): |
1090 | + db_snap = getUtility(ISnapSet).getByName(self.person, snap["name"]) |
1091 | + db_builds = list(db_snap.builds) |
1092 | + db_builds[0].updateStatus( |
1093 | + BuildStatus.BUILDING, date_started=db_snap.date_created) |
1094 | + db_builds[0].updateStatus( |
1095 | + BuildStatus.FULLYBUILT, |
1096 | + date_finished=db_snap.date_created + timedelta(minutes=10)) |
1097 | + snap = self.webservice.get(snap["self_link"]).jsonBody() |
1098 | + # Builds that have not yet been started are listed last. This does |
1099 | + # mean that pending builds that have never been started are sorted |
1100 | + # to the end, but means that builds that were cancelled before |
1101 | + # starting don't pollute the start of the collection forever. |
1102 | + self.assertEqual(builds, self.getCollectionLinks(snap, "builds")) |
1103 | + self.assertEqual( |
1104 | + builds[:1], self.getCollectionLinks(snap, "completed_builds")) |
1105 | + self.assertEqual( |
1106 | + builds[1:], self.getCollectionLinks(snap, "pending_builds")) |
1107 | + |
1108 | + with person_logged_in(self.person): |
1109 | + db_builds[1].updateStatus( |
1110 | + BuildStatus.BUILDING, date_started=db_snap.date_created) |
1111 | + db_builds[1].updateStatus( |
1112 | + BuildStatus.FULLYBUILT, |
1113 | + date_finished=db_snap.date_created + timedelta(minutes=20)) |
1114 | + snap = self.webservice.get(snap["self_link"]).jsonBody() |
1115 | + self.assertEqual( |
1116 | + [builds[1], builds[0], builds[2], builds[3]], |
1117 | + self.getCollectionLinks(snap, "builds")) |
1118 | + self.assertEqual( |
1119 | + [builds[1], builds[0]], |
1120 | + self.getCollectionLinks(snap, "completed_builds")) |
1121 | + self.assertEqual( |
1122 | + builds[2:], self.getCollectionLinks(snap, "pending_builds")) |
1123 | + |
1124 | + def test_query_count(self): |
1125 | + # Snap has a reasonable query count. |
1126 | + snap = self.factory.makeSnap(registrant=self.person, owner=self.person) |
1127 | + url = api_url(snap) |
1128 | + logout() |
1129 | + store = Store.of(snap) |
1130 | + store.flush() |
1131 | + store.invalidate() |
1132 | + with StormStatementRecorder() as recorder: |
1133 | + self.webservice.get(url) |
1134 | + self.assertThat(recorder, HasQueryCount(Equals(15))) |
1135 | |
1136 | === modified file 'lib/lp/snappy/tests/test_snapbuild.py' |
1137 | --- lib/lp/snappy/tests/test_snapbuild.py 2015-08-03 09:54:47 +0000 |
1138 | +++ lib/lp/snappy/tests/test_snapbuild.py 2015-08-03 13:17:37 +0000 |
1139 | @@ -5,17 +5,28 @@ |
1140 | |
1141 | __metaclass__ = type |
1142 | |
1143 | -from datetime import timedelta |
1144 | +from datetime import ( |
1145 | + datetime, |
1146 | + timedelta, |
1147 | + ) |
1148 | +from urllib2 import ( |
1149 | + HTTPError, |
1150 | + urlopen, |
1151 | + ) |
1152 | |
1153 | +import pytz |
1154 | from zope.component import getUtility |
1155 | from zope.security.proxy import removeSecurityProxy |
1156 | |
1157 | from lp.app.errors import NotFoundError |
1158 | +from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
1159 | from lp.buildmaster.enums import BuildStatus |
1160 | from lp.buildmaster.interfaces.buildqueue import IBuildQueue |
1161 | from lp.buildmaster.interfaces.packagebuild import IPackageBuild |
1162 | from lp.registry.enums import PersonVisibility |
1163 | from lp.services.features.testing import FeatureFixture |
1164 | +from lp.services.librarian.browser import ProxiedLibraryFileAlias |
1165 | +from lp.services.webapp.interfaces import OAuthPermission |
1166 | from lp.snappy.interfaces.snap import ( |
1167 | SNAP_FEATURE_FLAG, |
1168 | SnapFeatureDisabled, |
1169 | @@ -26,10 +37,19 @@ |
1170 | ) |
1171 | from lp.soyuz.enums import ArchivePurpose |
1172 | from lp.testing import ( |
1173 | + ANONYMOUS, |
1174 | + api_url, |
1175 | + login, |
1176 | + logout, |
1177 | person_logged_in, |
1178 | TestCaseWithFactory, |
1179 | ) |
1180 | -from lp.testing.layers import LaunchpadZopelessLayer |
1181 | +from lp.testing.layers import ( |
1182 | + LaunchpadFunctionalLayer, |
1183 | + LaunchpadZopelessLayer, |
1184 | + ) |
1185 | +from lp.testing.mail_helpers import pop_notifications |
1186 | +from lp.testing.pages import webservice_for_person |
1187 | |
1188 | |
1189 | class TestSnapBuildFeatureFlag(TestCaseWithFactory): |
1190 | @@ -224,3 +244,160 @@ |
1191 | def test_getByBuildFarmJobs_works_empty(self): |
1192 | self.assertContentEqual( |
1193 | [], getUtility(ISnapBuildSet).getByBuildFarmJobs([])) |
1194 | + |
1195 | + |
1196 | +class TestSnapBuildWebservice(TestCaseWithFactory): |
1197 | + |
1198 | + layer = LaunchpadFunctionalLayer |
1199 | + |
1200 | + def setUp(self): |
1201 | + super(TestSnapBuildWebservice, self).setUp() |
1202 | + self.useFixture(FeatureFixture({SNAP_FEATURE_FLAG: u"on"})) |
1203 | + self.person = self.factory.makePerson() |
1204 | + self.webservice = webservice_for_person( |
1205 | + self.person, permission=OAuthPermission.WRITE_PRIVATE) |
1206 | + self.webservice.default_api_version = "devel" |
1207 | + login(ANONYMOUS) |
1208 | + |
1209 | + def getURL(self, obj): |
1210 | + return self.webservice.getAbsoluteUrl(api_url(obj)) |
1211 | + |
1212 | + def test_properties(self): |
1213 | + # The basic properties of a SnapBuild are sensible. |
1214 | + db_build = self.factory.makeSnapBuild( |
1215 | + requester=self.person, |
1216 | + date_created=datetime(2014, 04, 25, 10, 38, 0, tzinfo=pytz.UTC)) |
1217 | + build_url = api_url(db_build) |
1218 | + logout() |
1219 | + build = self.webservice.get(build_url).jsonBody() |
1220 | + with person_logged_in(self.person): |
1221 | + self.assertEqual(self.getURL(self.person), build["requester_link"]) |
1222 | + self.assertEqual(self.getURL(db_build.snap), build["snap_link"]) |
1223 | + self.assertEqual( |
1224 | + self.getURL(db_build.archive), build["archive_link"]) |
1225 | + self.assertEqual( |
1226 | + self.getURL(db_build.distro_arch_series), |
1227 | + build["distro_arch_series_link"]) |
1228 | + self.assertEqual("Release", build["pocket"]) |
1229 | + self.assertIsNone(build["score"]) |
1230 | + self.assertFalse(build["can_be_rescored"]) |
1231 | + self.assertFalse(build["can_be_cancelled"]) |
1232 | + |
1233 | + def test_public(self): |
1234 | + # A SnapBuild with a public Snap and archive is itself public. |
1235 | + db_build = self.factory.makeSnapBuild() |
1236 | + build_url = api_url(db_build) |
1237 | + unpriv_webservice = webservice_for_person( |
1238 | + self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC) |
1239 | + unpriv_webservice.default_api_version = "devel" |
1240 | + logout() |
1241 | + self.assertEqual(200, self.webservice.get(build_url).status) |
1242 | + self.assertEqual(200, unpriv_webservice.get(build_url).status) |
1243 | + |
1244 | + def test_private_snap(self): |
1245 | + # A SnapBuild with a private Snap is private. |
1246 | + db_team = self.factory.makeTeam( |
1247 | + owner=self.person, visibility=PersonVisibility.PRIVATE) |
1248 | + with person_logged_in(self.person): |
1249 | + db_build = self.factory.makeSnapBuild( |
1250 | + requester=self.person, owner=db_team) |
1251 | + build_url = api_url(db_build) |
1252 | + unpriv_webservice = webservice_for_person( |
1253 | + self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC) |
1254 | + unpriv_webservice.default_api_version = "devel" |
1255 | + logout() |
1256 | + self.assertEqual(200, self.webservice.get(build_url).status) |
1257 | + # 404 since we aren't allowed to know that the private team exists. |
1258 | + self.assertEqual(404, unpriv_webservice.get(build_url).status) |
1259 | + |
1260 | + def test_private_archive(self): |
1261 | + # A SnapBuild with a private archive is private. |
1262 | + db_archive = self.factory.makeArchive(owner=self.person, private=True) |
1263 | + with person_logged_in(self.person): |
1264 | + db_build = self.factory.makeSnapBuild(archive=db_archive) |
1265 | + build_url = api_url(db_build) |
1266 | + unpriv_webservice = webservice_for_person( |
1267 | + self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC) |
1268 | + unpriv_webservice.default_api_version = "devel" |
1269 | + logout() |
1270 | + self.assertEqual(200, self.webservice.get(build_url).status) |
1271 | + self.assertEqual(401, unpriv_webservice.get(build_url).status) |
1272 | + |
1273 | + def test_cancel(self): |
1274 | + # The owner of a build can cancel it. |
1275 | + db_build = self.factory.makeSnapBuild(requester=self.person) |
1276 | + db_build.queueBuild() |
1277 | + build_url = api_url(db_build) |
1278 | + unpriv_webservice = webservice_for_person( |
1279 | + self.factory.makePerson(), permission=OAuthPermission.WRITE_PUBLIC) |
1280 | + unpriv_webservice.default_api_version = "devel" |
1281 | + logout() |
1282 | + build = self.webservice.get(build_url).jsonBody() |
1283 | + self.assertTrue(build["can_be_cancelled"]) |
1284 | + response = unpriv_webservice.named_post(build["self_link"], "cancel") |
1285 | + self.assertEqual(401, response.status) |
1286 | + response = self.webservice.named_post(build["self_link"], "cancel") |
1287 | + self.assertEqual(200, response.status) |
1288 | + build = self.webservice.get(build_url).jsonBody() |
1289 | + self.assertFalse(build["can_be_cancelled"]) |
1290 | + with person_logged_in(self.person): |
1291 | + self.assertEqual(BuildStatus.CANCELLED, db_build.status) |
1292 | + |
1293 | + def test_rescore(self): |
1294 | + # Buildd administrators can rescore builds. |
1295 | + db_build = self.factory.makeSnapBuild(requester=self.person) |
1296 | + db_build.queueBuild() |
1297 | + build_url = api_url(db_build) |
1298 | + buildd_admin = self.factory.makePerson( |
1299 | + member_of=[getUtility(ILaunchpadCelebrities).buildd_admin]) |
1300 | + buildd_admin_webservice = webservice_for_person( |
1301 | + buildd_admin, permission=OAuthPermission.WRITE_PUBLIC) |
1302 | + buildd_admin_webservice.default_api_version = "devel" |
1303 | + logout() |
1304 | + build = self.webservice.get(build_url).jsonBody() |
1305 | + self.assertEqual(2505, build["score"]) |
1306 | + self.assertTrue(build["can_be_rescored"]) |
1307 | + response = self.webservice.named_post( |
1308 | + build["self_link"], "rescore", score=5000) |
1309 | + self.assertEqual(401, response.status) |
1310 | + response = buildd_admin_webservice.named_post( |
1311 | + build["self_link"], "rescore", score=5000) |
1312 | + self.assertEqual(200, response.status) |
1313 | + build = self.webservice.get(build_url).jsonBody() |
1314 | + self.assertEqual(5000, build["score"]) |
1315 | + |
1316 | + def assertCanOpenRedirectedUrl(self, browser, url): |
1317 | + redirection = self.assertRaises(HTTPError, browser.open, url) |
1318 | + self.assertEqual(303, redirection.code) |
1319 | + urlopen(redirection.hdrs["Location"]).close() |
1320 | + |
1321 | + def test_logs(self): |
1322 | + # API clients can fetch the build and upload logs. |
1323 | + db_build = self.factory.makeSnapBuild(requester=self.person) |
1324 | + db_build.setLog(self.factory.makeLibraryFileAlias("buildlog.txt.gz")) |
1325 | + db_build.storeUploadLog("uploaded") |
1326 | + build_url = api_url(db_build) |
1327 | + logout() |
1328 | + build = self.webservice.get(build_url).jsonBody() |
1329 | + browser = self.getNonRedirectingBrowser(user=self.person) |
1330 | + self.assertIsNotNone(build["build_log_url"]) |
1331 | + self.assertCanOpenRedirectedUrl(browser, build["build_log_url"]) |
1332 | + self.assertIsNotNone(build["upload_log_url"]) |
1333 | + self.assertCanOpenRedirectedUrl(browser, build["upload_log_url"]) |
1334 | + |
1335 | + def test_getFileUrls(self): |
1336 | + # API clients can fetch files attached to builds. |
1337 | + db_build = self.factory.makeSnapBuild(requester=self.person) |
1338 | + db_files = [ |
1339 | + self.factory.makeSnapFile(snapbuild=db_build) for i in range(2)] |
1340 | + build_url = api_url(db_build) |
1341 | + file_urls = [ |
1342 | + ProxiedLibraryFileAlias(file.libraryfile, db_build).http_url |
1343 | + for file in db_files] |
1344 | + logout() |
1345 | + response = self.webservice.named_get(build_url, "getFileUrls") |
1346 | + self.assertEqual(200, response.status) |
1347 | + self.assertContentEqual(file_urls, response.jsonBody()) |
1348 | + browser = self.getNonRedirectingBrowser(user=self.person) |
1349 | + for file_url in file_urls: |
1350 | + self.assertCanOpenRedirectedUrl(browser, file_url) |
1351 | |
1352 | === modified file 'lib/lp/soyuz/browser/tests/test_livefsbuild.py' |
1353 | --- lib/lp/soyuz/browser/tests/test_livefsbuild.py 2014-06-17 13:04:31 +0000 |
1354 | +++ lib/lp/soyuz/browser/tests/test_livefsbuild.py 2015-08-03 13:17:37 +0000 |
1355 | @@ -230,17 +230,12 @@ |
1356 | build.buildqueue_record.logtail = "tail of the log" |
1357 | return build |
1358 | |
1359 | - def makeNonRedirectingBrowser(self, url, user=None): |
1360 | - browser = setupBrowserForUser(user) if user else setupBrowser() |
1361 | - browser.mech_browser.set_handle_equiv(False) |
1362 | - browser.open(url) |
1363 | - return browser |
1364 | - |
1365 | def test_builder_index_public(self): |
1366 | build = self.makeBuildingLiveFS() |
1367 | builder_url = canonical_url(build.builder) |
1368 | logout() |
1369 | - browser = self.makeNonRedirectingBrowser(builder_url) |
1370 | + browser = self.getNonRedirectingBrowser( |
1371 | + url=builder_url, user=ANONYMOUS) |
1372 | self.assertIn("tail of the log", browser.contents) |
1373 | |
1374 | def test_builder_index_private(self): |
1375 | @@ -248,14 +243,13 @@ |
1376 | with admin_logged_in(): |
1377 | build = self.makeBuildingLiveFS(archive=archive) |
1378 | builder_url = canonical_url(build.builder) |
1379 | - user = self.factory.makePerson() |
1380 | logout() |
1381 | |
1382 | # An unrelated user can't see the logtail of a private build. |
1383 | - browser = self.makeNonRedirectingBrowser(builder_url, user=user) |
1384 | + browser = self.getNonRedirectingBrowser(url=builder_url) |
1385 | self.assertNotIn("tail of the log", browser.contents) |
1386 | |
1387 | # But someone who can see the archive can. |
1388 | - browser = self.makeNonRedirectingBrowser( |
1389 | - builder_url, user=archive.owner) |
1390 | + browser = self.getNonRedirectingBrowser( |
1391 | + url=builder_url, user=archive.owner) |
1392 | self.assertIn("tail of the log", browser.contents) |
1393 | |
1394 | === modified file 'lib/lp/soyuz/tests/test_livefsbuild.py' |
1395 | --- lib/lp/soyuz/tests/test_livefsbuild.py 2015-04-20 09:48:57 +0000 |
1396 | +++ lib/lp/soyuz/tests/test_livefsbuild.py 2015-08-03 13:17:37 +0000 |
1397 | @@ -17,8 +17,6 @@ |
1398 | import pytz |
1399 | from zope.component import getUtility |
1400 | from zope.security.proxy import removeSecurityProxy |
1401 | -from zope.testbrowser.browser import Browser |
1402 | -from zope.testbrowser.testing import PublisherMechanizeBrowser |
1403 | |
1404 | from lp.app.errors import NotFoundError |
1405 | from lp.app.interfaces.launchpad import ILaunchpadCelebrities |
1406 | @@ -318,14 +316,6 @@ |
1407 | [], getUtility(ILiveFSBuildSet).getByBuildFarmJobs([])) |
1408 | |
1409 | |
1410 | -class NonRedirectingMechanizeBrowser(PublisherMechanizeBrowser): |
1411 | - """A `mechanize.Browser` that does not handle redirects.""" |
1412 | - |
1413 | - default_features = [ |
1414 | - feature for feature in PublisherMechanizeBrowser.default_features |
1415 | - if feature != "_redirect"] |
1416 | - |
1417 | - |
1418 | class TestLiveFSBuildWebservice(TestCaseWithFactory): |
1419 | |
1420 | layer = LaunchpadFunctionalLayer |
1421 | @@ -452,17 +442,6 @@ |
1422 | build = self.webservice.get(build_url).jsonBody() |
1423 | self.assertEqual(5000, build["score"]) |
1424 | |
1425 | - def makeNonRedirectingBrowser(self, person): |
1426 | - # The test browser can only work with the appserver, not the |
1427 | - # librarian, so follow one layer of redirection through the |
1428 | - # appserver and then ask the librarian for the real file. |
1429 | - browser = Browser(mech_browser=NonRedirectingMechanizeBrowser()) |
1430 | - browser.handleErrors = False |
1431 | - with person_logged_in(person): |
1432 | - browser.addHeader( |
1433 | - "Authorization", "Basic %s:test" % person.preferredemail.email) |
1434 | - return browser |
1435 | - |
1436 | def assertCanOpenRedirectedUrl(self, browser, url): |
1437 | redirection = self.assertRaises(HTTPError, browser.open, url) |
1438 | self.assertEqual(303, redirection.code) |
1439 | @@ -476,7 +455,7 @@ |
1440 | build_url = api_url(db_build) |
1441 | logout() |
1442 | build = self.webservice.get(build_url).jsonBody() |
1443 | - browser = self.makeNonRedirectingBrowser(self.person) |
1444 | + browser = self.getNonRedirectingBrowser(user=self.person) |
1445 | self.assertIsNotNone(build["build_log_url"]) |
1446 | self.assertCanOpenRedirectedUrl(browser, build["build_log_url"]) |
1447 | self.assertIsNotNone(build["upload_log_url"]) |
1448 | @@ -496,6 +475,6 @@ |
1449 | response = self.webservice.named_get(build_url, "getFileUrls") |
1450 | self.assertEqual(200, response.status) |
1451 | self.assertContentEqual(file_urls, response.jsonBody()) |
1452 | - browser = self.makeNonRedirectingBrowser(self.person) |
1453 | + browser = self.getNonRedirectingBrowser(user=self.person) |
1454 | for file_url in file_urls: |
1455 | self.assertCanOpenRedirectedUrl(browser, file_url) |
1456 | |
1457 | === modified file 'lib/lp/soyuz/tests/test_packageupload.py' |
1458 | --- lib/lp/soyuz/tests/test_packageupload.py 2014-07-18 07:43:06 +0000 |
1459 | +++ lib/lp/soyuz/tests/test_packageupload.py 2015-08-03 13:17:37 +0000 |
1460 | @@ -20,8 +20,6 @@ |
1461 | from zope.schema import getFields |
1462 | from zope.security.interfaces import Unauthorized as ZopeUnauthorized |
1463 | from zope.security.proxy import removeSecurityProxy |
1464 | -from zope.testbrowser.browser import Browser |
1465 | -from zope.testbrowser.testing import PublisherMechanizeBrowser |
1466 | |
1467 | from lp.archiveuploader.tests import datadir |
1468 | from lp.registry.interfaces.pocket import PackagePublishingPocket |
1469 | @@ -861,14 +859,6 @@ |
1470 | self.assertEqual(PackageUploadStatus.REJECTED, pu.status) |
1471 | |
1472 | |
1473 | -class NonRedirectingMechanizeBrowser(PublisherMechanizeBrowser): |
1474 | - """A `mechanize.Browser` that does not handle redirects.""" |
1475 | - |
1476 | - default_features = [ |
1477 | - feature for feature in PublisherMechanizeBrowser.default_features |
1478 | - if feature != "_redirect"] |
1479 | - |
1480 | - |
1481 | class TestPackageUploadWebservice(TestCaseWithFactory): |
1482 | """Test the exposure of queue methods to the web service.""" |
1483 | |
1484 | @@ -947,17 +937,6 @@ |
1485 | transaction.commit() |
1486 | return upload, self.load(upload, person) |
1487 | |
1488 | - def makeNonRedirectingBrowser(self, person): |
1489 | - # The test browser can only work with the appserver, not the |
1490 | - # librarian, so follow one layer of redirection through the |
1491 | - # appserver and then ask the librarian for the real file. |
1492 | - browser = Browser(mech_browser=NonRedirectingMechanizeBrowser()) |
1493 | - browser.handleErrors = False |
1494 | - with person_logged_in(person): |
1495 | - browser.addHeader( |
1496 | - "Authorization", "Basic %s:test" % person.preferredemail.email) |
1497 | - return browser |
1498 | - |
1499 | def assertCanOpenRedirectedUrl(self, browser, url): |
1500 | redirection = self.assertRaises(HTTPError, browser.open, url) |
1501 | self.assertEqual(303, redirection.code) |
1502 | @@ -1042,7 +1021,7 @@ |
1503 | for file in upload.sourcepackagerelease.files] |
1504 | self.assertContentEqual(source_file_urls, ws_source_file_urls) |
1505 | |
1506 | - browser = self.makeNonRedirectingBrowser(person) |
1507 | + browser = self.getNonRedirectingBrowser(user=person) |
1508 | for ws_source_file_url in ws_source_file_urls: |
1509 | self.assertCanOpenRedirectedUrl(browser, ws_source_file_url) |
1510 | self.assertCanOpenRedirectedUrl(browser, ws_upload.changes_file_url) |
1511 | @@ -1131,7 +1110,7 @@ |
1512 | for file in bpr.files] |
1513 | self.assertContentEqual(binary_file_urls, ws_binary_file_urls) |
1514 | |
1515 | - browser = self.makeNonRedirectingBrowser(person) |
1516 | + browser = self.getNonRedirectingBrowser(user=person) |
1517 | for ws_binary_file_url in ws_binary_file_urls: |
1518 | self.assertCanOpenRedirectedUrl(browser, ws_binary_file_url) |
1519 | self.assertCanOpenRedirectedUrl(browser, ws_upload.changes_file_url) |
1520 | @@ -1294,7 +1273,7 @@ |
1521 | for file in upload.customfiles] |
1522 | self.assertContentEqual(custom_file_urls, ws_custom_file_urls) |
1523 | |
1524 | - browser = self.makeNonRedirectingBrowser(person) |
1525 | + browser = self.getNonRedirectingBrowser(user=person) |
1526 | for ws_custom_file_url in ws_custom_file_urls: |
1527 | self.assertCanOpenRedirectedUrl(browser, ws_custom_file_url) |
1528 | self.assertCanOpenRedirectedUrl(browser, ws_upload.changes_file_url) |
1529 | |
1530 | === modified file 'lib/lp/testing/__init__.py' |
1531 | --- lib/lp/testing/__init__.py 2015-07-21 09:04:01 +0000 |
1532 | +++ lib/lp/testing/__init__.py 2015-08-03 13:17:37 +0000 |
1533 | @@ -778,6 +778,18 @@ |
1534 | browser.open(url) |
1535 | return browser |
1536 | |
1537 | + def getNonRedirectingBrowser(self, url=None, user=None): |
1538 | + from lp.testing.pages import setupBrowser |
1539 | + if user == ANONYMOUS: |
1540 | + browser = setupBrowser() |
1541 | + else: |
1542 | + browser = self.getUserBrowser(user=user) |
1543 | + browser.mech_browser.set_handle_redirect(False) |
1544 | + browser.mech_browser.set_handle_equiv(False) |
1545 | + if url is not None: |
1546 | + browser.open(url) |
1547 | + return browser |
1548 | + |
1549 | def createBranchAtURL(self, branch_url, format=None): |
1550 | """Create a branch at the supplied URL. |
1551 |