Merge lp:~abentley/launchpad/ppa-api into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 13204
Proposed branch: lp:~abentley/launchpad/ppa-api
Merge into: lp:launchpad
Diff against target: 1234 lines (+408/-254)
11 files modified
lib/canonical/launchpad/interfaces/_schema_circular_imports.py (+9/-1)
lib/lp/soyuz/browser/archive.py (+10/-70)
lib/lp/soyuz/browser/tests/archive-views.txt (+12/-9)
lib/lp/soyuz/browser/tests/test_archive_webservice.py (+119/-2)
lib/lp/soyuz/doc/archive-dependencies.txt (+10/-108)
lib/lp/soyuz/doc/archive.txt (+4/-5)
lib/lp/soyuz/interfaces/archive.py (+127/-46)
lib/lp/soyuz/model/archive.py (+36/-6)
lib/lp/soyuz/stories/webservice/xx-archive.txt (+4/-0)
lib/lp/soyuz/tests/test_archive.py (+72/-6)
lib/lp/testing/factory.py (+5/-1)
To merge this branch: bzr merge lp:~abentley/launchpad/ppa-api
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+63601@code.launchpad.net

This proposal supersedes a proposal from 2011-06-01.

Description of the change

Summary
=======
This code was approved and merged, but then rolled back because there was validation in the browser code that was missing from the model code. The changes from the previous version are here:

http://pastebin.ubuntu.com/620212/

Original summary: Fix bug #776444 and #776449 about missing APIs

Proposed change
===============
Export APIs

Implementation Details
======================
Implemented _addArchiveDependency as a wrapper, because we do not want to export IComponent.

Also fixed the permissions of addArchiveDependency and removeArchiveDependency.

Moved the validation code from the view to the model, so that the API respects it.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/canonical/launchpad/interfaces/_schema_circular_imports.py
  lib/lp/soyuz/browser/archive.py
  lib/lp/soyuz/browser/tests/archive-views.txt
  lib/lp/soyuz/browser/tests/test_archive_webservice.py
  lib/lp/soyuz/doc/archive-dependencies.txt
  lib/lp/soyuz/doc/archive.txt
  lib/lp/soyuz/interfaces/archive.py
  lib/lp/soyuz/model/archive.py
  lib/lp/soyuz/stories/webservice/xx-archive.txt
  lib/lp/soyuz/tests/test_archive.py
  lib/lp/testing/factory.py

./lib/lp/soyuz/browser/tests/archive-views.txt
       1: narrative uses a moin header.
       9: narrative uses a moin header.
     486: narrative uses a moin header.
     529: narrative uses a moin header.
     656: narrative uses a moin header.
    1040: narrative uses a moin header.
    1317: narrative uses a moin header.
    1383: narrative uses a moin header.
./lib/lp/soyuz/doc/archive-dependencies.txt
       1: narrative uses a moin header.
       8: narrative uses a moin header.
      30: narrative uses a moin header.
      81: narrative uses a moin header.
     156: narrative uses a moin header.
     330: narrative uses a moin header.
     449: narrative uses a moin header.
     479: narrative uses a moin header.
./lib/lp/soyuz/stories/webservice/xx-archive.txt
      20: want exceeds 78 characters.
      87: want exceeds 78 characters.
     113: source exceeds 78 characters.
     120: narrative uses a moin header.
     131: source exceeds 78 characters.
     140: want exceeds 78 characters.
     145: source exceeds 78 characters.
     155: want exceeds 78 characters.
     160: source exceeds 78 characters.
     170: want exceeds 78 characters.
     175: source exceeds 78 characters.
     185: want exceeds 78 characters.
     189: narrative uses a moin header.
     272: want exceeds 78 characters.
     333: want exceeds 78 characters.
     341: narrative has trailing whitespace.
     346: source has trailing whitespace.
     374: source has trailing whitespace.
     464: want exceeds 78 characters.
     490: narrative uses a moin header.
     497: source exceeds 78 characters.
     506: source exceeds 78 characters.
     514: source exceeds 78 characters.
     520: narrative exceeds 78 characters.
     523: source exceeds 78 characters.
     530: narrative uses a moin header.
     558: narrative uses a moin header.
     587: narrative uses a moin header.
     766: narrative uses a moin header.
     768: narrative exceeds 78 characters.
     787: source exceeds 78 characters.
     797: narrative uses a moin header.
     824: want exceeds 78 characters.
     840: want exceeds 78 characters.
     854: narrative uses a moin header.
     943: narrative uses a moin header.
     956: narrative uses a moin header.
    1004: narrative uses a moin header.

Checking for conflicts and issues in changed files.

To post a comment you must log in.
Revision history for this message
Gavin Panella (allenap) wrote : Posted in a previous version of this proposal

Looks good :)

[1]

+ with ExpectedException(LRUnauthorized, '.*'):
+ ws_archive.lp_save()

Oh wow, that's neat.

[2]

+class InvalidExternalDependencies(Exception):
+ """Tried to set external dependencies to an invalid value."""
+
+ webservice_error(400) # Bad request.
+
+ def __init__(self, errors):
+ error_msg = 'Invalid external dependencies:\n%s\n' % '\n'.join(errors)
+ Exception.__init__(self, error_msg)

Although it's a single line of inheritance, Exception is a new style
class, so please use super() here.

review: Approve
Revision history for this message
Aaron Bentley (abentley) wrote : Posted in a previous version of this proposal

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

On 11-06-02 11:43 AM, Gavin Panella wrote:
> Review: Approve
> Looks good :)
>
>
> [1]
>
> + with ExpectedException(LRUnauthorized, '.*'):
> + ws_archive.lp_save()
>
> Oh wow, that's neat.

Thanks. I added that to testtools myself. I like that it preserves the
normal way of writing function calls, so it's easier to read than
assertRaises.

> [2]
>
> +class InvalidExternalDependencies(Exception):
> + """Tried to set external dependencies to an invalid value."""
> +
> + webservice_error(400) # Bad request.
> +
> + def __init__(self, errors):
> + error_msg = 'Invalid external dependencies:\n%s\n' % '\n'.join(errors)
> + Exception.__init__(self, error_msg)
>
> Although it's a single line of inheritance, Exception is a new style
> class, so please use super() here.

Thanks. I guess I missed that when we switched to python 2.5.

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk3nx1kACgkQ0F+nu1YWqI3i0gCggzwz0ihRu1BrK/hkHFoxTxD/
wWMAn2WOlUv7dDndqxNDmcfPEVg2+HbS
=tvir
-----END PGP SIGNATURE-----

Revision history for this message
Graham Binns (gmb) wrote :

r=me on the latest changes.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/interfaces/_schema_circular_imports.py'
2--- lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-06-09 08:07:52 +0000
3+++ lib/canonical/launchpad/interfaces/_schema_circular_imports.py 2011-06-09 17:59:33 +0000
4@@ -1,4 +1,4 @@
5-# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
6+# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
7 # GNU Affero General Public License version 3 (see the file LICENSE).
8
9 """Update the interface schema values due to circular imports.
10@@ -434,6 +434,14 @@
11 IArchive, 'getUploadersForPackageset', 'packageset', IPackageset)
12 patch_plain_parameter_type(
13 IArchive, 'deletePackagesetUploader', 'packageset', IPackageset)
14+patch_plain_parameter_type(
15+ IArchive, 'removeArchiveDependency', 'dependency', IArchive)
16+patch_plain_parameter_type(
17+ IArchive, '_addArchiveDependency', 'dependency', IArchive)
18+patch_choice_parameter_type(
19+ IArchive, '_addArchiveDependency', 'pocket', PackagePublishingPocket)
20+patch_entry_return_type(
21+ IArchive, '_addArchiveDependency', IArchiveDependency)
22
23
24 # IBuildFarmJob
25
26=== modified file 'lib/lp/soyuz/browser/archive.py'
27--- lib/lp/soyuz/browser/archive.py 2011-06-09 08:07:52 +0000
28+++ lib/lp/soyuz/browser/archive.py 2011-06-09 17:59:33 +0000
29@@ -33,7 +33,6 @@
30 datetime,
31 timedelta,
32 )
33-from urlparse import urlparse
34
35 import pytz
36 from sqlobject import SQLObjectNotFound
37@@ -138,11 +137,13 @@
38 PackagePublishingStatus,
39 )
40 from lp.soyuz.interfaces.archive import (
41+ ArchiveDependencyError,
42 CannotCopy,
43 IArchive,
44 IArchiveEditDependenciesForm,
45 IArchiveSet,
46 NoSuchPPA,
47+ validate_external_dependencies,
48 )
49 from lp.soyuz.interfaces.archivepermission import IArchivePermissionSet
50 from lp.soyuz.interfaces.archivesubscriber import IArchiveSubscriberSet
51@@ -1864,45 +1865,6 @@
52 self._messages.append(structured(
53 '<p>Primary dependency added: %s</p>', primary_dependency.title))
54
55- def validate(self, data):
56- """Validate dependency configuration changes.
57-
58- Skip checks if no dependency candidate was sent in the form.
59-
60- Validate if the requested PPA dependency is sane (different than
61- the context PPA and not yet registered).
62-
63- Also check if the dependency candidate is private, if so, it can
64- only be set if the user has 'launchpad.View' permission on it and
65- the context PPA is also private (this way P3A credentials will be
66- sanitized from buildlogs).
67- """
68- dependency_candidate = data.get('dependency_candidate')
69-
70- if dependency_candidate is None:
71- return
72-
73- if dependency_candidate == self.context:
74- self.setFieldError('dependency_candidate',
75- "An archive should not depend on itself.")
76- return
77-
78- if self.context.getArchiveDependency(dependency_candidate):
79- self.setFieldError('dependency_candidate',
80- "This dependency is already registered.")
81- return
82-
83- if not check_permission('launchpad.View', dependency_candidate):
84- self.setFieldError(
85- 'dependency_candidate',
86- "You don't have permission to use this dependency.")
87- return
88-
89- if dependency_candidate.private and not self.context.private:
90- self.setFieldError(
91- 'dependency_candidate',
92- "Public PPAs cannot depend on private ones.")
93-
94 @action(_("Save"), name="save")
95 def save_action(self, action, data):
96 """Save dependency configuration changes.
97@@ -1914,18 +1876,21 @@
98 refreshing. And render a page notification with the summary of the
99 changes made.
100 """
101- # Redirect after POST.
102- self.next_url = self.request.URL
103-
104 # Process the form.
105 self._add_primary_dependencies(data)
106- self._add_ppa_dependencies(data)
107+ try:
108+ self._add_ppa_dependencies(data)
109+ except ArchiveDependencyError as e:
110+ self.setFieldError('dependency_candidate', str(e))
111+ return
112 self._remove_dependencies(data)
113
114 # Issue a notification if anything was changed.
115 if len(self.messages) > 0:
116 self.request.response.addNotification(
117 structured(self.messages))
118+ # Redirect after POST.
119+ self.next_url = self.request.URL
120
121
122 class ArchiveActivateView(LaunchpadFormView):
123@@ -2128,7 +2093,7 @@
124 # Check the external_dependencies field.
125 ext_deps = data.get('external_dependencies')
126 if ext_deps is not None:
127- errors = self.validate_external_dependencies(ext_deps)
128+ errors = validate_external_dependencies(ext_deps)
129 if len(errors) != 0:
130 error_text = "\n".join(errors)
131 self.setFieldError('external_dependencies', error_text)
132@@ -2138,31 +2103,6 @@
133 'commercial',
134 'Can only set commericial for private archives.')
135
136- def validate_external_dependencies(self, ext_deps):
137- """Validate the external_dependencies field.
138-
139- :param ext_deps: The dependencies form field to check.
140- """
141- errors = []
142- # The field can consist of multiple entries separated by
143- # newlines, so process each in turn.
144- for dep in ext_deps.splitlines():
145- try:
146- deb, url, suite, components = dep.split(" ", 3)
147- except ValueError:
148- errors.append(
149- "'%s' is not a complete and valid sources.list entry"
150- % dep)
151- continue
152-
153- if deb != "deb":
154- errors.append("%s: Must start with 'deb'" % dep)
155- url_components = urlparse(url)
156- if not url_components[0] or not url_components[1]:
157- errors.append("%s: Invalid URL" % dep)
158-
159- return errors
160-
161 @property
162 def owner_is_private_team(self):
163 """Is the owner a private team?
164
165=== modified file 'lib/lp/soyuz/browser/tests/archive-views.txt'
166--- lib/lp/soyuz/browser/tests/archive-views.txt 2011-06-06 02:00:42 +0000
167+++ lib/lp/soyuz/browser/tests/archive-views.txt 2011-06-09 17:59:33 +0000
168@@ -753,7 +753,8 @@
169 mark/ppa
170
171 >>> print dependency.title.escapedtext
172- <a href="http://launchpad.dev/~mark/+archive/ppa">PPA for Mark Shuttleworth</a>
173+ <a href="http://launchpad.dev/~mark/+archive/ppa">PPA for Mark
174+ Shuttleworth</a>
175
176 The form focus, now that we have a recorded dependencies, is set to the
177 first listed dependency.
178@@ -1387,32 +1388,34 @@
179 >>> ppa_archive_view = create_initialized_view(
180 ... cprov.archive, name="+admin")
181
182-The validate_external_dependencies() method is called when validating and will
183-return a list of errors if the data dis not validate. A valid entry is of the
184-form:
185+ >>> from lp.soyuz.interfaces.archive import validate_external_dependencies
186+
187+The validate_external_dependencies() function is called when validating and
188+will return a list of errors if the data dis not validate. A valid entry is
189+of the form:
190 deb scheme://domain/ suite component[s]
191
192- >>> print ppa_archive_view.validate_external_dependencies(
193+ >>> print validate_external_dependencies(
194 ... "deb http://example.com/ karmic main")
195 []
196
197 Multiple entries are valid, separated by newlines:
198
199- >>> print ppa_archive_view.validate_external_dependencies(
200+ >>> print validate_external_dependencies(
201 ... "deb http://example.com/ karmic main\n"
202 ... "deb http://example.com/ karmic restricted")
203 []
204
205 If the line does not start with the word "deb" it fails:
206
207- >>> print ppa_archive_view.validate_external_dependencies(
208+ >>> print validate_external_dependencies(
209 ... "deb http://example.com/ karmic universe\n"
210 ... "dab http://example.com/ karmic main")
211 ["dab http://example.com/ karmic main: Must start with 'deb'"]
212
213 If the line has too few parts it fails. Here we're missing a suite:
214
215- >>> print ppa_archive_view.validate_external_dependencies(
216+ >>> print validate_external_dependencies(
217 ... "deb http://example.com/ karmic universe\n"
218 ... "deb http://example.com/ main")
219 ["'deb http://example.com/ main'
220@@ -1420,7 +1423,7 @@
221
222 If the URL looks invalid, it fails:
223
224- >>> print ppa_archive_view.validate_external_dependencies(
225+ >>> print validate_external_dependencies(
226 ... "deb http://example.com/ karmic universe\n"
227 ... "deb example.com/ karmic main")
228 ['deb example.com/ karmic main: Invalid URL']
229
230=== modified file 'lib/lp/soyuz/browser/tests/test_archive_webservice.py'
231--- lib/lp/soyuz/browser/tests/test_archive_webservice.py 2011-06-06 02:00:42 +0000
232+++ lib/lp/soyuz/browser/tests/test_archive_webservice.py 2011-06-09 17:59:33 +0000
233@@ -1,19 +1,30 @@
234-# Copyright 2010 Canonical Ltd. This software is licensed under the
235+# Copyright 2010-2011 Canonical Ltd. This software is licensed under the
236 # GNU Affero General Public License version 3 (see the file LICENSE).
237
238 __metaclass__ = type
239
240 import unittest
241
242-from lazr.restfulclient.errors import HTTPError
243+from lazr.restfulclient.errors import (
244+ BadRequest,
245+ HTTPError,
246+ Unauthorized as LRUnauthorized,
247+)
248+from testtools import ExpectedException
249+import transaction
250+from zope.component import getUtility
251
252 from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller
253 from canonical.testing.layers import DatabaseFunctionalLayer
254+from lp.app.interfaces.launchpad import ILaunchpadCelebrities
255+from lp.registry.interfaces.pocket import PackagePublishingPocket
256 from lp.soyuz.enums import ArchivePurpose
257 from lp.testing import (
258 celebrity_logged_in,
259 launchpadlib_for,
260+ person_logged_in,
261 TestCaseWithFactory,
262+ WebServiceTestCase,
263 )
264
265
266@@ -67,5 +78,111 @@
267 "in the 'DEVELOPMENT' state.", e.content)
268
269
270+class TestExternalDependencies(WebServiceTestCase):
271+
272+ def test_external_dependencies_random_user(self):
273+ """Normal users can look but not touch."""
274+ archive = self.factory.makeArchive()
275+ transaction.commit()
276+ ws_archive = self.wsObject(archive)
277+ self.assertIs(None, ws_archive.external_dependencies)
278+ ws_archive.external_dependencies = "random"
279+ with ExpectedException(LRUnauthorized, '.*'):
280+ ws_archive.lp_save()
281+
282+ def test_external_dependencies_owner(self):
283+ """Normal archive owners can look but not touch."""
284+ archive = self.factory.makeArchive()
285+ transaction.commit()
286+ ws_archive = self.wsObject(archive, archive.owner)
287+ self.assertIs(None, ws_archive.external_dependencies)
288+ ws_archive.external_dependencies = "random"
289+ with ExpectedException(LRUnauthorized, '.*'):
290+ ws_archive.lp_save()
291+
292+ def test_external_dependencies_commercial_owner_invalid(self):
293+ """Commercial admins can look and touch."""
294+ commercial = getUtility(ILaunchpadCelebrities).commercial_admin
295+ owner = self.factory.makePerson(member_of=[commercial])
296+ archive = self.factory.makeArchive(owner=owner)
297+ transaction.commit()
298+ ws_archive = self.wsObject(archive, archive.owner)
299+ self.assertIs(None, ws_archive.external_dependencies)
300+ ws_archive.external_dependencies = "random"
301+ regex = '(\n|.)*Invalid external dependencies(\n|.)*'
302+ with ExpectedException(BadRequest, regex):
303+ ws_archive.lp_save()
304+
305+ def test_external_dependencies_commercial_owner_valid(self):
306+ """Commercial admins can look and touch."""
307+ commercial = getUtility(ILaunchpadCelebrities).commercial_admin
308+ owner = self.factory.makePerson(member_of=[commercial])
309+ archive = self.factory.makeArchive(owner=owner)
310+ transaction.commit()
311+ ws_archive = self.wsObject(archive, archive.owner)
312+ self.assertIs(None, ws_archive.external_dependencies)
313+ ws_archive.external_dependencies = (
314+ "deb http://example.org suite components")
315+ ws_archive.lp_save()
316+
317+
318+class TestArchiveDependencies(WebServiceTestCase):
319+
320+ def test_addArchiveDependency_random_user(self):
321+ """Normal users cannot add archive dependencies."""
322+ archive = self.factory.makeArchive()
323+ dependency = self.factory.makeArchive()
324+ transaction.commit()
325+ ws_archive = self.wsObject(archive)
326+ ws_dependency = self.wsObject(dependency)
327+ self.assertContentEqual([], ws_archive.dependencies)
328+ failure_regex = '(.|\n)*addArchiveDependency.*launchpad.Edit(.|\n)*'
329+ with ExpectedException(LRUnauthorized, failure_regex):
330+ dependency = ws_archive.addArchiveDependency(
331+ dependency=ws_dependency, pocket='Release', component='main')
332+
333+ def test_addArchiveDependency_owner(self):
334+ """Normal users cannot add archive dependencies."""
335+ archive = self.factory.makeArchive()
336+ dependency = self.factory.makeArchive()
337+ transaction.commit()
338+ ws_archive = self.wsObject(archive, archive.owner)
339+ ws_dependency = self.wsObject(dependency)
340+ self.assertContentEqual([], ws_archive.dependencies)
341+ with ExpectedException(BadRequest, '(.|\n)*asdf(.|\n)*'):
342+ ws_archive.addArchiveDependency(
343+ dependency=ws_dependency, pocket='Release', component='asdf')
344+ dependency = ws_archive.addArchiveDependency(
345+ dependency=ws_dependency, pocket='Release', component='main')
346+ self.assertContentEqual([dependency], ws_archive.dependencies)
347+
348+ def test_removeArchiveDependency_random_user(self):
349+ """Normal users can remove archive dependencies."""
350+ archive = self.factory.makeArchive()
351+ dependency = self.factory.makeArchive()
352+ with person_logged_in(archive.owner):
353+ archive.addArchiveDependency(
354+ dependency, PackagePublishingPocket.RELEASE)
355+ transaction.commit()
356+ ws_archive = self.wsObject(archive)
357+ ws_dependency = self.wsObject(dependency)
358+ failure_regex = '(.|\n)*remove.*Dependency.*launchpad.Edit(.|\n)*'
359+ with ExpectedException(LRUnauthorized, failure_regex):
360+ ws_archive.removeArchiveDependency(dependency=ws_dependency)
361+
362+ def test_removeArchiveDependency_owner(self):
363+ """Normal users can remove archive dependencies."""
364+ archive = self.factory.makeArchive()
365+ dependency = self.factory.makeArchive()
366+ with person_logged_in(archive.owner):
367+ archive.addArchiveDependency(
368+ dependency, PackagePublishingPocket.RELEASE)
369+ transaction.commit()
370+ ws_archive = self.wsObject(archive, archive.owner)
371+ ws_dependency = self.wsObject(dependency)
372+ ws_archive.removeArchiveDependency(dependency=ws_dependency)
373+ self.assertContentEqual([], ws_archive.dependencies)
374+
375+
376 def test_suite():
377 return unittest.TestLoader().loadTestsFromName(__name__)
378
379=== modified file 'lib/lp/soyuz/doc/archive-dependencies.txt'
380--- lib/lp/soyuz/doc/archive-dependencies.txt 2011-06-07 00:16:31 +0000
381+++ lib/lp/soyuz/doc/archive-dependencies.txt 2011-06-09 17:59:33 +0000
382@@ -281,115 +281,17 @@
383 deb http://archive.launchpad.dev/ubuntu hoary-updates
384 main restricted universe multiverse
385
386-The authentication information gets added for private PPA
387-dependencies. We'll create a private PPA and then update cprov's PPA,
388-removing the dependency on Mark's public PPA and adding one for the
389-new private PPA.
390-
391- >>> private_ppa = factory.makeArchive(
392- ... owner=mark, name='p3a', private=True, distribution=ubuntu)
393- >>> pub_binaries = test_publisher.getPubBinaries(
394- ... binaryname='dep-bin', archive=private_ppa,
395- ... status=PackagePublishingStatus.PUBLISHED)
396 >>> cprov.archive.removeArchiveDependency(mark.archive)
397- >>> archive_dependency = cprov.archive.addArchiveDependency(
398- ... private_ppa, PackagePublishingPocket.RELEASE,
399- ... getUtility(IComponentSet)['main'])
400-
401- >>> print_building_sources_list(a_build)
402- deb http://ppa.launchpad.dev/cprov/ppa/ubuntu hoary main
403- deb http://buildd:sekrit@private-ppa.launchpad.dev/mark/p3a/ubuntu
404- hoary main
405- deb http://archive.launchpad.dev/ubuntu hoary
406- main restricted universe multiverse
407- deb http://archive.launchpad.dev/ubuntu hoary-security
408- main restricted universe multiverse
409- deb http://archive.launchpad.dev/ubuntu hoary-updates
410- main restricted universe multiverse
411-
412-Remove the private PPA dependency before continuing.
413-
414- >>> cprov.archive.removeArchiveDependency(private_ppa)
415-
416- >>> cprov.archive.external_dependencies is None
417- True
418- >>> cprov.archive.enabled
419- True
420-
421-What happens when we fake external dependencies?
422-
423- >>> cprov.archive.external_dependencies = (
424- ... "Malformed format string here --> %(series)")
425-
426-The error caused by the malformed format string is caught and the other
427-dependencies are returned as normal.
428-
429- >>> print_building_sources_list(a_build)
430- ERROR:root:Exception during external dependency processing:
431- Traceback (most recent call last):
432- File ... in get_sources_list_for_building
433- {'series': distroarchseries.distroseries.name})
434- ValueError: incomplete format
435- <BLANKLINE>
436- deb http://ppa.launchpad.dev/cprov/ppa/ubuntu hoary main
437- deb http://archive.launchpad.dev/ubuntu hoary
438- main restricted universe multiverse
439- deb http://archive.launchpad.dev/ubuntu hoary-security
440- main restricted universe multiverse
441- deb http://archive.launchpad.dev/ubuntu hoary-updates
442- main restricted universe multiverse
443-
444-However, in order to avoid the problem going forward (and to allow the PPA
445-owner to correct the external dependencies) the PPA will be disabled.
446-
447- >>> cprov.archive.enabled
448- False
449-
450-Restore the 'external_dependencies' property to its original state and
451-re-enable the PPA.
452-
453- >>> cprov.archive.external_dependencies = None
454- >>> cprov.archive.enable()
455-
456- >>> cprov.archive.external_dependencies is None
457- True
458- >>> cprov.archive.enabled
459- True
460-
461-What happens when we fake external dependencies?
462-
463- >>> cprov.archive.external_dependencies = (
464- ... "Malformed format string here --> %(series)")
465-
466-The error caused by the malformed format string is caught and the other
467-dependencies are returned as normal.
468-
469- >>> print_building_sources_list(a_build)
470- ERROR:root:Exception during external dependency processing:
471- Traceback (most recent call last):
472- File ... in get_sources_list_for_building
473- {'series': distroarchseries.distroseries.name})
474- ValueError: incomplete format
475- <BLANKLINE>
476- deb http://ppa.launchpad.dev/cprov/ppa/ubuntu hoary main
477- deb http://archive.launchpad.dev/ubuntu hoary
478- main restricted universe multiverse
479- deb http://archive.launchpad.dev/ubuntu hoary-security
480- main restricted universe multiverse
481- deb http://archive.launchpad.dev/ubuntu hoary-updates
482- main restricted universe multiverse
483-
484-However, in order to avoid the problem going forward (and to allow the PPA
485-owner to correct the external dependencies) the PPA will be disabled.
486-
487- >>> cprov.archive.enabled
488- False
489-
490-Restore the 'external_dependencies' property to its original state and
491-re-enable the PPA.
492-
493- >>> cprov.archive.external_dependencies = None
494- >>> cprov.archive.enable()
495+
496+What when we supply invalid dependencies, an exception is raised.
497+
498+ >>> cprov.archive.external_dependencies = (
499+ ... "Malformed format string here --> %(series)")
500+ Traceback (most recent call last):
501+ InvalidExternalDependencies: (InvalidExternalDependencies(...),
502+ "Invalid external dependencies:\nMalformed format string here
503+ --> %(series): Must start with 'deb'\nMalformed format string here
504+ --> %(series): Invalid URL\n")
505
506
507 == Overriding default primary archive dependencies ==
508
509=== modified file 'lib/lp/soyuz/doc/archive.txt'
510--- lib/lp/soyuz/doc/archive.txt 2011-06-06 02:00:42 +0000
511+++ lib/lp/soyuz/doc/archive.txt 2011-06-09 17:59:33 +0000
512@@ -101,7 +101,7 @@
513
514 >>> login("admin@canonical.com")
515 >>> cprov_archive.relative_build_score = 100
516- >>> cprov_archive.external_dependencies = "test"
517+ >>> cprov_archive.external_dependencies = "deb http://foo hardy bar"
518
519 The buildd_secret is used by the slave scanner when generating a
520 sources.list entry for the builder to access a private archive. It is
521@@ -1043,6 +1043,7 @@
522 >>> print_dependencies(no_priv.archive)
523 No dependencies recorded.
524
525+ >>> login_person(no_priv)
526 >>> archive_dependency = no_priv.archive.addArchiveDependency(
527 ... cprov.archive, release_pocket, main_component)
528
529@@ -1058,8 +1059,7 @@
530 ... cprov.archive, release_pocket, main_component)
531 Traceback (most recent call last):
532 ...
533- ArchiveDependencyError: Only one dependency record per archive is
534- supported.
535+ ArchiveDependencyError: This dependency is already registered.
536
537 'dependency' and target archive are the same.
538
539@@ -1099,8 +1099,7 @@
540 ... getUtility(IComponentSet)['main'])
541 Traceback (most recent call last):
542 ...
543- ArchiveDependencyError: Only one dependency record per archive is
544- supported.
545+ ArchiveDependencyError: This dependency is already registered.
546
547 Thus archive dependency removal can be performed simply by passing the
548 dependency target.
549
550=== modified file 'lib/lp/soyuz/interfaces/archive.py'
551--- lib/lp/soyuz/interfaces/archive.py 2011-06-07 00:16:31 +0000
552+++ lib/lp/soyuz/interfaces/archive.py 2011-06-09 17:59:33 +0000
553@@ -32,6 +32,7 @@
554 'IDistributionArchive',
555 'InsufficientUploadRights',
556 'InvalidComponent',
557+ 'InvalidExternalDependencies',
558 'InvalidPocketForPartnerArchive',
559 'InvalidPocketForPPA',
560 'IPPA',
561@@ -43,8 +44,12 @@
562 'PocketNotFound',
563 'VersionRequiresName',
564 'default_name_by_purpose',
565+ 'validate_external_dependencies',
566 ]
567
568+
569+from urlparse import urlparse
570+
571 from lazr.enum import DBEnumeratedType
572 from lazr.restful.declarations import (
573 call_with,
574@@ -54,6 +59,7 @@
575 export_read_operation,
576 export_write_operation,
577 exported,
578+ operation_for_version,
579 operation_parameters,
580 operation_returns_collection_of,
581 operation_returns_entry,
582@@ -114,59 +120,59 @@
583
584 class CannotCopy(Exception):
585 """Exception raised when a copy cannot be performed."""
586- webservice_error(400) #Bad request.
587+ webservice_error(400) # Bad request.
588
589
590 class CannotSwitchPrivacy(Exception):
591 """Raised when switching the privacy of an archive that has
592 publishing records."""
593- webservice_error(400) # Bad request.
594+ webservice_error(400) # Bad request.
595
596
597 class PocketNotFound(Exception):
598 """Invalid pocket."""
599- webservice_error(400) #Bad request.
600+ webservice_error(400) # Bad request.
601
602
603 class DistroSeriesNotFound(Exception):
604 """Invalid distroseries."""
605- webservice_error(400) #Bad request.
606+ webservice_error(400) # Bad request.
607
608
609 class AlreadySubscribed(Exception):
610 """Raised when creating a subscription for a subscribed person."""
611- webservice_error(400) # Bad request.
612+ webservice_error(400) # Bad request.
613
614
615 class ArchiveNotPrivate(Exception):
616 """Raised when creating an archive subscription for a public archive."""
617- webservice_error(400) # Bad request.
618+ webservice_error(400) # Bad request.
619
620
621 class NoTokensForTeams(Exception):
622 """Raised when creating a token for a team, rather than a person."""
623- webservice_error(400) # Bad request.
624+ webservice_error(400) # Bad request.
625
626
627 class ComponentNotFound(Exception):
628 """Invalid source name."""
629- webservice_error(400) #Bad request.
630+ webservice_error(400) # Bad request.
631
632
633 class InvalidComponent(Exception):
634 """Invalid component name."""
635- webservice_error(400) #Bad request.
636+ webservice_error(400) # Bad request.
637
638
639 class NoSuchPPA(NameLookupFailed):
640 """Raised when we try to look up an PPA that doesn't exist."""
641- webservice_error(400) #Bad request.
642+ webservice_error(400) # Bad request.
643 _message_prefix = "No such ppa"
644
645
646 class VersionRequiresName(Exception):
647 """Raised on some queries when version is specified but name is not."""
648- webservice_error(400) # Bad request.
649+ webservice_error(400) # Bad request.
650
651
652 class CannotRestrictArchitectures(Exception):
653@@ -175,7 +181,7 @@
654
655 class CannotUploadToArchive(Exception):
656 """A reason for not being able to upload to an archive."""
657- webservice_error(403) # Forbidden.
658+ webservice_error(403) # Forbidden.
659
660 _fmt = '%(person)s has no upload rights to %(archive)s.'
661
662@@ -192,7 +198,7 @@
663
664 class CannotUploadToPocket(Exception):
665 """Returned when a pocket is closed for uploads."""
666- webservice_error(403) # Forbidden.
667+ webservice_error(403) # Forbidden.
668
669 def __init__(self, distroseries, pocket):
670 Exception.__init__(self,
671@@ -248,6 +254,17 @@
672 CannotUploadToArchive.__init__(self, archive_name=archive_name)
673
674
675+class InvalidExternalDependencies(Exception):
676+ """Tried to set external dependencies to an invalid value."""
677+
678+ webservice_error(400) # Bad request.
679+
680+ def __init__(self, errors):
681+ error_msg = 'Invalid external dependencies:\n%s\n' % '\n'.join(errors)
682+ super(Exception, self).__init__(self, error_msg)
683+ self.errors = errors
684+
685+
686 class IArchivePublic(IHasOwner, IPrivacy):
687 """An Archive interface for publicly available operations."""
688 id = Attribute("The archive ID.")
689@@ -332,7 +349,7 @@
690
691 distribution = exported(
692 Reference(
693- Interface, # Redefined to IDistribution later.
694+ Interface, # Redefined to IDistribution later.
695 title=_("The distribution that uses or is used by this "
696 "archive.")))
697
698@@ -412,9 +429,9 @@
699 "A delta to apply to all build scores for the archive. Builds "
700 "with a higher score will build sooner."))
701
702- external_dependencies = Text(
703- title=_("External dependencies"), required=False, readonly=False,
704- description=_(
705+ external_dependencies = exported(
706+ Text(title=_("External dependencies"), required=False,
707+ readonly=False, description=_(
708 "Newline-separated list of repositories to be used to retrieve "
709 "any external build dependencies when building packages in the "
710 "archive, in the format:\n"
711@@ -422,7 +439,7 @@
712 "[components]\n"
713 "The series variable is replaced with the series name of the "
714 "context build.\n"
715- "NOTE: This is for migration of OEM PPAs only!"))
716+ "NOTE: This is for migration of OEM PPAs only!")))
717
718 enabled_restricted_families = CollectionField(
719 title=_("Enabled restricted families"),
720@@ -520,27 +537,6 @@
721 records.
722 """
723
724- def removeArchiveDependency(dependency):
725- """Remove the `IArchiveDependency` record for the given dependency.
726-
727- :param dependency: is an `IArchive` object.
728- """
729-
730- def addArchiveDependency(dependency, pocket, component=None):
731- """Record an archive dependency record for the context archive.
732-
733- :param dependency: is an `IArchive` object.
734- :param pocket: is an `PackagePublishingPocket` enum.
735- :param component: is an optional `IComponent` object, if not given
736- the archive dependency will be tied to the component used
737- for a corresponding source in primary archive.
738-
739- :raise: `ArchiveDependencyError` if given 'dependency' does not fit
740- the context archive.
741- :return: a `IArchiveDependency` object targeted to the context
742- `IArchive` requiring 'dependency' `IArchive`.
743- """
744-
745 def getPermissions(person, item, perm_type):
746 """Get the `IArchivePermission` record with the supplied details.
747
748@@ -892,7 +888,8 @@
749 :return: True if the person is allowed to upload the source package.
750 """
751
752- num_pkgs_building = Attribute("Tuple of packages building and waiting to build")
753+ num_pkgs_building = Attribute(
754+ "Tuple of packages building and waiting to build")
755
756 def getSourcePackageReleases(build_status=None):
757 """Return the releases for this archive.
758@@ -944,7 +941,8 @@
759 dependencies = exported(
760 CollectionField(
761 title=_("Archive dependencies recorded for this archive."),
762- value_type=Reference(schema=Interface), #Really IArchiveDependency
763+ value_type=Reference(schema=Interface),
764+ # Really IArchiveDependency
765 readonly=True))
766
767 description = exported(
768@@ -1111,8 +1109,8 @@
769 """
770
771 @operation_parameters(
772- dependency=Reference(schema=Interface)) #Really IArchive. See below.
773- @operation_returns_entry(schema=Interface) #Really IArchiveDependency.
774+ dependency=Reference(schema=Interface)) # Really IArchive. See below.
775+ @operation_returns_entry(schema=Interface) # Really IArchiveDependency.
776 @export_read_operation()
777 def getArchiveDependency(dependency):
778 """Return the `IArchiveDependency` object for the given dependency.
779@@ -1233,7 +1231,8 @@
780 source_names=List(
781 title=_("Source package names"),
782 value_type=TextLine()),
783- from_archive=Reference(schema=Interface), #Really IArchive, see below
784+ from_archive=Reference(schema=Interface),
785+ #Really IArchive, see below
786 to_pocket=TextLine(title=_("Pocket name")),
787 to_series=TextLine(title=_("Distroseries name"), required=False),
788 include_binaries=Bool(
789@@ -1275,7 +1274,8 @@
790 @operation_parameters(
791 source_name=TextLine(title=_("Source package name")),
792 version=TextLine(title=_("Version")),
793- from_archive=Reference(schema=Interface), #Really IArchive, see below
794+ from_archive=Reference(schema=Interface),
795+ # Really IArchive, see below
796 to_pocket=TextLine(title=_("Pocket name")),
797 to_series=TextLine(title=_("Distroseries name"), required=False),
798 include_binaries=Bool(
799@@ -1314,7 +1314,7 @@
800
801 @call_with(registrant=REQUEST_USER)
802 @operation_parameters(
803- subscriber = PublicPersonChoice(
804+ subscriber=PublicPersonChoice(
805 title=_("Subscriber"),
806 required=True,
807 vocabulary='ValidPersonOrTeam',
808@@ -1458,6 +1458,61 @@
809 processed.
810 """
811
812+ def addArchiveDependency(dependency, pocket, component=None):
813+ """Record an archive dependency record for the context archive.
814+
815+ :param dependency: is an `IArchive` object.
816+ :param pocket: is an `PackagePublishingPocket` enum.
817+ :param component: is an optional `IComponent` object, if not given
818+ the archive dependency will be tied to the component used
819+ for a corresponding source in primary archive.
820+
821+ :raise: `ArchiveDependencyError` if given 'dependency' does not fit
822+ the context archive.
823+ :return: a `IArchiveDependency` object targeted to the context
824+ `IArchive` requiring 'dependency' `IArchive`.
825+ """
826+
827+ @operation_parameters(
828+ dependency=Reference(schema=Interface, required=True),
829+ # Really IArchive
830+ pocket=Choice(
831+ title=_("Pocket"),
832+ description=_("The pocket into which this entry is published"),
833+ # Really PackagePublishingPocket.
834+ vocabulary=DBEnumeratedType,
835+ required=True),
836+ component=TextLine(title=_("Component"), required=False),
837+ )
838+ @export_operation_as('addArchiveDependency')
839+ @export_factory_operation(Interface, []) # Really IArchiveDependency
840+ @operation_for_version('devel')
841+ def _addArchiveDependency(dependency, pocket, component=None):
842+ """Record an archive dependency record for the context archive.
843+
844+ :param dependency: is an `IArchive` object.
845+ :param pocket: is an `PackagePublishingPocket` enum.
846+ :param component: is the name of a component. If not given,
847+ the archive dependency will be tied to the component used
848+ for a corresponding source in primary archive.
849+
850+ :raise: `ArchiveDependencyError` if given 'dependency' does not fit
851+ the context archive.
852+ :return: a `IArchiveDependency` object targeted to the context
853+ `IArchive` requiring 'dependency' `IArchive`.
854+ """
855+ @operation_parameters(
856+ dependency=Reference(schema=Interface, required=True),
857+ # Really IArchive
858+ )
859+ @export_write_operation()
860+ @operation_for_version('devel')
861+ def removeArchiveDependency(dependency):
862+ """Remove the `IArchiveDependency` record for the given dependency.
863+
864+ :param dependency: is an `IArchive` object.
865+ """
866+
867
868 class IArchive(IArchivePublic, IArchiveAppend, IArchiveEdit, IArchiveView):
869 """Main Archive interface."""
870@@ -1690,3 +1745,29 @@
871 )
872
873 # Circular dependency issues fixed in _schema_circular_imports.py
874+
875+
876+def validate_external_dependencies(ext_deps):
877+ """Validate the external_dependencies field.
878+
879+ :param ext_deps: The dependencies form field to check.
880+ """
881+ errors = []
882+ # The field can consist of multiple entries separated by
883+ # newlines, so process each in turn.
884+ for dep in ext_deps.splitlines():
885+ try:
886+ deb, url, suite, components = dep.split(" ", 3)
887+ except ValueError:
888+ errors.append(
889+ "'%s' is not a complete and valid sources.list entry"
890+ % dep)
891+ continue
892+
893+ if deb != "deb":
894+ errors.append("%s: Must start with 'deb'" % dep)
895+ url_components = urlparse(url)
896+ if not url_components[0] or not url_components[1]:
897+ errors.append("%s: Invalid URL" % dep)
898+
899+ return errors
900
901=== modified file 'lib/lp/soyuz/model/archive.py'
902--- lib/lp/soyuz/model/archive.py 2011-06-07 00:16:31 +0000
903+++ lib/lp/soyuz/model/archive.py 2011-06-09 17:59:33 +0000
904@@ -68,6 +68,7 @@
905 ISlaveStore,
906 IStore,
907 )
908+from canonical.launchpad.webapp.authorization import check_permission
909 from canonical.launchpad.webapp.interfaces import (
910 DEFAULT_FLAVOR,
911 IStoreSelector,
912@@ -125,6 +126,7 @@
913 CannotSwitchPrivacy,
914 CannotUploadToPocket,
915 CannotUploadToPPA,
916+ ComponentNotFound,
917 default_name_by_purpose,
918 DistroSeriesNotFound,
919 FULL_COMPONENT_SUPPORT,
920@@ -133,6 +135,7 @@
921 IDistributionArchive,
922 InsufficientUploadRights,
923 InvalidComponent,
924+ InvalidExternalDependencies,
925 InvalidPocketForPartnerArchive,
926 InvalidPocketForPPA,
927 IPPA,
928@@ -143,6 +146,7 @@
929 NoTokensForTeams,
930 PocketNotFound,
931 VersionRequiresName,
932+ validate_external_dependencies,
933 )
934 from lp.soyuz.interfaces.archivearch import IArchiveArchSet
935 from lp.soyuz.interfaces.archiveauthtoken import IArchiveAuthTokenSet
936@@ -197,6 +201,14 @@
937 from lp.soyuz.model.sourcepackagerelease import SourcePackageRelease
938
939
940+def storm_validate_external_dependencies(archive, attr, value):
941+ assert attr == 'external_dependencies'
942+ errors = validate_external_dependencies(value)
943+ if len(errors) > 0:
944+ raise InvalidExternalDependencies(errors)
945+ return value
946+
947+
948 class Archive(SQLBase):
949 implements(IArchive, IHasOwner, IHasBuildRecords)
950 _table = 'Archive'
951@@ -306,7 +318,8 @@
952 # Launchpad and should be re-examined in October 2010 to see if it
953 # is still relevant.
954 external_dependencies = StringCol(
955- dbName='external_dependencies', notNull=False, default=None)
956+ dbName='external_dependencies', notNull=False, default=None,
957+ storm_validator=storm_validate_external_dependencies)
958
959 commercial = BoolCol(
960 dbName='commercial', notNull=True, default=False)
961@@ -483,7 +496,7 @@
962
963 if name is not None:
964 if exact_match:
965- storm_clauses.append(SourcePackageName.name==name)
966+ storm_clauses.append(SourcePackageName.name == name)
967 else:
968 clauses.append(
969 "SourcePackageName.name LIKE '%%%%' || %s || '%%%%'"
970@@ -494,7 +507,7 @@
971 raise VersionRequiresName(
972 "The 'version' parameter can be used only together with"
973 " the 'name' parameter.")
974- storm_clauses.append(SourcePackageRelease.version==version)
975+ storm_clauses.append(SourcePackageRelease.version == version)
976 else:
977 orderBy.insert(1, Desc(SourcePackageRelease.version))
978
979@@ -514,7 +527,7 @@
980
981 if pocket is not None:
982 storm_clauses.append(
983- SourcePackagePublishingHistory.pocket==pocket)
984+ SourcePackagePublishingHistory.pocket == pocket)
985
986 if created_since_date is not None:
987 clauses.append(
988@@ -529,6 +542,7 @@
989 *orderBy)
990 if not eager_load:
991 return resultset
992+
993 # Its not clear that this eager load is necessary or sufficient, it
994 # replaces a prejoin that had pathological query plans.
995 def eager_load(rows):
996@@ -609,7 +623,7 @@
997 clauseTables = ['SourcePackageRelease', 'SourcePackageName']
998
999 order_const = "SourcePackageRelease.version"
1000- desc_version_order = SQLConstant(order_const+" DESC")
1001+ desc_version_order = SQLConstant(order_const + " DESC")
1002 orderBy = ['SourcePackageName.name', desc_version_order,
1003 '-SourcePackagePublishingHistory.id']
1004
1005@@ -953,7 +967,14 @@
1006 a_dependency = self.getArchiveDependency(dependency)
1007 if a_dependency is not None:
1008 raise ArchiveDependencyError(
1009- "Only one dependency record per archive is supported.")
1010+ "This dependency is already registered.")
1011+ if not check_permission('launchpad.View', dependency):
1012+ raise ArchiveDependencyError(
1013+ "You don't have permission to use this dependency.")
1014+ return
1015+ if dependency.private and not self.private:
1016+ raise ArchiveDependencyError(
1017+ "Public PPAs cannot depend on private ones.")
1018
1019 if dependency.is_ppa:
1020 if pocket is not PackagePublishingPocket.RELEASE:
1021@@ -969,6 +990,15 @@
1022 archive=self, dependency=dependency, pocket=pocket,
1023 component=component)
1024
1025+ def _addArchiveDependency(self, dependency, pocket, component=None):
1026+ """See `IArchive`."""
1027+ if isinstance(component, basestring):
1028+ try:
1029+ component = getUtility(IComponentSet)[component]
1030+ except NotFoundError as e:
1031+ raise ComponentNotFound(e)
1032+ return self.addArchiveDependency(dependency, pocket, component)
1033+
1034 def getPermissions(self, user, item, perm_type):
1035 """See `IArchive`."""
1036 permission_set = getUtility(IArchivePermissionSet)
1037
1038=== modified file 'lib/lp/soyuz/stories/webservice/xx-archive.txt'
1039--- lib/lp/soyuz/stories/webservice/xx-archive.txt 2011-06-06 02:00:42 +0000
1040+++ lib/lp/soyuz/stories/webservice/xx-archive.txt 2011-06-09 17:59:33 +0000
1041@@ -21,6 +21,7 @@
1042 description: u'packages to help my friends.'
1043 displayname: u'PPA for Celso Providelo'
1044 distribution_link: u'http://.../ubuntu'
1045+ external_dependencies: None
1046 name: u'ppa'
1047 owner_link: u'http://.../~cprov'
1048 private: False
1049@@ -87,6 +88,7 @@
1050 description: None
1051 displayname: u'Primary Archive for Ubuntu Test'
1052 distribution_link: u'http://.../ubuntutest'
1053+ external_dependencies: None
1054 name: u'primary'
1055 owner_link: u'http://.../~ubuntu-team'
1056 private: False
1057@@ -823,6 +825,7 @@
1058 description: u'tag:launchpad.net:2008:redacted'
1059 displayname: u'PPA named p3a for Celso Providelo'
1060 distribution_link: u'http://.../ubuntu'
1061+ external_dependencies: None
1062 name: u'p3a'
1063 owner_link: u'http://.../~cprov'
1064 private: True
1065@@ -838,6 +841,7 @@
1066 description: u'packages to help my friends.'
1067 displayname: u'PPA named p3a for Celso Providelo'
1068 distribution_link: u'http://.../ubuntu'
1069+ external_dependencies: None
1070 name: u'p3a'
1071 owner_link: u'http://.../~cprov'
1072 private: True
1073
1074=== modified file 'lib/lp/soyuz/tests/test_archive.py'
1075--- lib/lp/soyuz/tests/test_archive.py 2011-06-03 11:00:55 +0000
1076+++ lib/lp/soyuz/tests/test_archive.py 2011-06-09 17:59:33 +0000
1077@@ -10,7 +10,9 @@
1078 )
1079 import doctest
1080
1081-from testtools.matchers import DocTestMatches
1082+from testtools.matchers import DocTestMatches, MatchesRegex
1083+from testtools.testcase import ExpectedException
1084+
1085 import transaction
1086 from zope.component import getUtility
1087 from zope.security.interfaces import Unauthorized
1088@@ -48,6 +50,7 @@
1089 PackagePublishingStatus,
1090 )
1091 from lp.soyuz.interfaces.archive import (
1092+ ArchiveDependencyError,
1093 ArchiveDisabled,
1094 CannotRestrictArchitectures,
1095 CannotUploadToPocket,
1096@@ -223,7 +226,7 @@
1097 # Calling series_with_sources returns all series with publishings.
1098 distribution = self.factory.makeDistribution()
1099 archive = self.factory.makeArchive(distribution=distribution)
1100- series_with_no_sources = self.factory.makeDistroSeries(
1101+ self.factory.makeDistroSeries(
1102 distribution=distribution, version="0.5")
1103 series_with_sources1 = self.factory.makeDistroSeries(
1104 distribution=distribution, version="1")
1105@@ -283,7 +286,7 @@
1106 source_package_release=sourcepackagerelease,
1107 archive=archive, status=status)
1108 sprs.append(sourcepackagerelease)
1109- unlinked_spr = self.factory.makeSourcePackageRelease()
1110+ self.factory.makeSourcePackageRelease()
1111 return archive, sprs
1112
1113 def test_getSourcePackageReleases_with_no_params(self):
1114@@ -425,7 +428,7 @@
1115 other_spn = self.factory.makeSourcePackageName(name="bar")
1116 archive = self.factory.makeArchive()
1117 self.makePublishedSources(archive,
1118- [PackagePublishingStatus.PUBLISHED]*3,
1119+ [PackagePublishingStatus.PUBLISHED] * 3,
1120 ["1.0", "1.1", "2.0"],
1121 [sourcepackagename, sourcepackagename, other_spn])
1122 pubs = removeSecurityProxy(archive)._collectLatestPublishedSources(
1123@@ -1299,6 +1302,69 @@
1124 self.assertTrue(self.archive.build_debug_symbols)
1125
1126
1127+class TestAddArchiveDependencies(TestCaseWithFactory):
1128+
1129+ layer = DatabaseFunctionalLayer
1130+
1131+ def test_add_hidden_dependency(self):
1132+ # The user cannot add a dependency on an archive they cannot see.
1133+ archive = self.factory.makeArchive(private=True)
1134+ dependency = self.factory.makeArchive(private=True)
1135+ with person_logged_in(archive.owner):
1136+ with ExpectedException(
1137+ ArchiveDependencyError,
1138+ "You don't have permission to use this dependency."):
1139+ archive.addArchiveDependency(dependency, 'foo')
1140+
1141+ def test_private_dependency_public_archive(self):
1142+ # A public archive may not depend on a private archive.
1143+ archive = self.factory.makeArchive()
1144+ dependency = self.factory.makeArchive(
1145+ private=True, owner=archive.owner)
1146+ with person_logged_in(archive.owner):
1147+ with ExpectedException(
1148+ ArchiveDependencyError,
1149+ "Public PPAs cannot depend on private ones."):
1150+ archive.addArchiveDependency(dependency, 'foo')
1151+
1152+ def test_add_private_dependency(self):
1153+ # The user can add a dependency on private archive they can see.
1154+ archive = self.factory.makeArchive(private=True)
1155+ dependency = self.factory.makeArchive(
1156+ private=True, owner=archive.owner)
1157+ with person_logged_in(archive.owner):
1158+ archive_dependency = archive.addArchiveDependency(dependency,
1159+ PackagePublishingPocket.RELEASE)
1160+ self.assertContentEqual(
1161+ archive.dependencies, [archive_dependency])
1162+
1163+
1164+class TestArchiveDependencies(TestCaseWithFactory):
1165+
1166+ layer = LaunchpadZopelessLayer
1167+
1168+ def test_private_sources_list(self):
1169+ """Entries for private dependencies include credentials."""
1170+ p3a = self.factory.makeArchive(name='p3a', private=True)
1171+ dependency = self.factory.makeArchive(
1172+ name='dependency', private=True, owner=p3a.owner)
1173+ with person_logged_in(p3a.owner):
1174+ bpph = self.factory.makeBinaryPackagePublishingHistory(
1175+ archive=dependency, status=PackagePublishingStatus.PUBLISHED)
1176+ p3a.addArchiveDependency(dependency,
1177+ PackagePublishingPocket.RELEASE)
1178+ build = self.factory.makeBinaryPackageBuild(archive=p3a,
1179+ distroarchseries=bpph.distroarchseries)
1180+ sources_list = get_sources_list_for_building(
1181+ build, build.distro_arch_series,
1182+ build.source_package_release.name)
1183+ sources_list_str = '\n'.join(sources_list)
1184+ matches = MatchesRegex(
1185+ "deb http://buildd:sekrit@private-ppa.launchpad.dev/"
1186+ "person-name-.*/dependency/ubuntu distroseries-.* main")
1187+ self.assertThat(sources_list[0], matches)
1188+
1189+
1190 class TestFindDepCandidates(TestCaseWithFactory):
1191 """Tests for Archive.findDepCandidates."""
1192
1193@@ -1346,7 +1412,7 @@
1194
1195 def test_does_not_find_pending_publication(self):
1196 # A pending candidate in the same archive should not be found.
1197- bins = self.publisher.getPubBinaries(
1198+ self.publisher.getPubBinaries(
1199 binaryname='foo', archive=self.archive)
1200 self.assertDep('i386', 'foo', [])
1201
1202@@ -1572,7 +1638,7 @@
1203 def test_two_ppas_with_team(self):
1204 team = self.factory.makeTeam(
1205 subscription_policy=TeamSubscriptionPolicy.MODERATED)
1206- ppa = self.factory.makeArchive(owner=team, name='ppa')
1207+ self.factory.makeArchive(owner=team, name='ppa')
1208 self.assertEqual("%s already has a PPA named 'ppa'." % (
1209 team.displayname), Archive.validatePPA(team, 'ppa'))
1210
1211
1212=== modified file 'lib/lp/testing/factory.py'
1213--- lib/lp/testing/factory.py 2011-06-09 08:07:52 +0000
1214+++ lib/lp/testing/factory.py 2011-06-09 17:59:33 +0000
1215@@ -595,7 +595,7 @@
1216 self, email=None, name=None, password=None,
1217 email_address_status=None, hide_email_addresses=False,
1218 displayname=None, time_zone=None, latitude=None, longitude=None,
1219- selfgenerated_bugnotifications=False):
1220+ selfgenerated_bugnotifications=False, member_of=()):
1221 """Create and return a new, arbitrary Person.
1222
1223 :param email: The email address for the new person.
1224@@ -660,6 +660,10 @@
1225
1226 self.makeOpenIdIdentifier(person.account)
1227
1228+ for team in member_of:
1229+ with person_logged_in(team.teamowner):
1230+ team.addMember(person, team.teamowner)
1231+
1232 # Ensure updated ValidPersonCache
1233 flush_database_updates()
1234 return person