Merge lp:~cjwatson/launchpad/snap-webservice into lp:launchpad

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
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