Merge lp:~thumper/launchpad/xmlrpc-lp-name-resolution into lp:launchpad

Proposed by Tim Penhey
Status: Merged
Approved by: Michael Hudson-Doyle
Approved revision: no longer in the source branch.
Merged at revision: 11516
Proposed branch: lp:~thumper/launchpad/xmlrpc-lp-name-resolution
Merge into: lp:launchpad
Diff against target: 582 lines (+171/-136)
4 files modified
lib/lp/code/interfaces/codehosting.py (+26/-0)
lib/lp/code/model/branch.py (+1/-18)
lib/lp/code/xmlrpc/branch.py (+42/-14)
lib/lp/code/xmlrpc/tests/test_branch.py (+102/-104)
To merge this branch: bzr merge lp:~thumper/launchpad/xmlrpc-lp-name-resolution
Reviewer Review Type Date Requested Status
Michael Hudson-Doyle Approve
Review via email: mp+34500@code.launchpad.net

Commit message

Resolve linked branches using the +branch alias over XMLRPC

Description of the change

This branch changes the way the XMLRPC server resolves the lp: urls where the alias for a branch is used. lp:~user/... are still resolved the same way, as are public http urls.
However for bzr+ssh urls, branches that are linked to series (either distroseries or product series) are now resolved to bzr+ssh://bazaar.launchpad.net/+branch/<alias> where <alias> is the bit after 'lp:' on the bazaar identity.

This is going to be used to allow private trunk branches to use the lp:<project> syntax for accessing branches.

tests:
  TestExpandURL

As a drive-by the compose_public_url function was moved into lp.code.interfaces.codehosting, which avoids the non-standard import statements that were there to avoid the circular dependencies.

Most of the changes are updates to the tests.

To post a comment you must log in.
Revision history for this message
Michael Hudson-Doyle (mwhudson) wrote :

Hi,

I'm a bit confused about the whole supported_schemes stuff, it seems overly hairy. But it's not new.

The docstring of assertRaises has become a bit garbled, can you have a proof read and fix it up?

Do we have a codehosting acceptance test that accesses a branch by its lp: urls? Maybe we should.

Otherwise all fine. People will be very happy about this :-)

Cheers,
mwh

review: Approve
Revision history for this message
Tim Penhey (thumper) wrote :

We do have acceptance tests that test that the name we resolve to works for branch access. That landed as part of the branch which supports the +branch alias file access over the VFS.

I'll look at the docstring.

Yes it is a bit hairy, but I was trying to work out how to best provide the URLs we are after without losing the flexibility to add something like the anon-bzr protocol.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/code/interfaces/codehosting.py'
--- lib/lp/code/interfaces/codehosting.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/interfaces/codehosting.py 2010-09-03 04:25:59 +0000
@@ -9,17 +9,24 @@
9__all__ = [9__all__ = [
10 'BRANCH_ALIAS_PREFIX',10 'BRANCH_ALIAS_PREFIX',
11 'BRANCH_TRANSPORT',11 'BRANCH_TRANSPORT',
12 'compose_public_url',
12 'CONTROL_TRANSPORT',13 'CONTROL_TRANSPORT',
13 'ICodehostingAPI',14 'ICodehostingAPI',
14 'ICodehostingApplication',15 'ICodehostingApplication',
15 'LAUNCHPAD_ANONYMOUS',16 'LAUNCHPAD_ANONYMOUS',
16 'LAUNCHPAD_SERVICES',17 'LAUNCHPAD_SERVICES',
17 'READ_ONLY',18 'READ_ONLY',
19 'SUPPORTED_SCHEMES',
18 'WRITABLE',20 'WRITABLE',
19 ]21 ]
2022
23import os.path
24import urllib
25
26from lazr.uri import URI
21from zope.interface import Interface27from zope.interface import Interface
2228
29from canonical.config import config
23from canonical.launchpad.validators.name import valid_name30from canonical.launchpad.validators.name import valid_name
24from canonical.launchpad.webapp.interfaces import ILaunchpadApplication31from canonical.launchpad.webapp.interfaces import ILaunchpadApplication
2532
@@ -49,6 +56,9 @@
49# The path prefix for getting at branches via their short name.56# The path prefix for getting at branches via their short name.
50BRANCH_ALIAS_PREFIX = '+branch'57BRANCH_ALIAS_PREFIX = '+branch'
5158
59# The scheme types that are supported for codehosting.
60SUPPORTED_SCHEMES = 'bzr+ssh', 'http'
61
5262
53class ICodehostingApplication(ILaunchpadApplication):63class ICodehostingApplication(ILaunchpadApplication):
54 """Branch Puller application root."""64 """Branch Puller application root."""
@@ -170,3 +180,19 @@
170 'path_in_transport' is a path relative to that transport. e.g.180 'path_in_transport' is a path relative to that transport. e.g.
171 (BRANCH_TRANSPORT, {'id': 3, 'writable': False}, '.bzr/README').181 (BRANCH_TRANSPORT, {'id': 3, 'writable': False}, '.bzr/README').
172 """182 """
183
184
185def compose_public_url(scheme, unique_name, suffix=None):
186 # Accept sftp as a legacy protocol.
187 accepted_schemes = set(SUPPORTED_SCHEMES)
188 accepted_schemes.add('sftp')
189 assert scheme in accepted_schemes, "Unknown scheme: %s" % scheme
190 host = URI(config.codehosting.supermirror_root).host
191 if isinstance(unique_name, unicode):
192 unique_name = unique_name.encode('utf-8')
193 # After quoting and encoding, the path should be perfectly
194 # safe as a plain ASCII string, str() just enforces this
195 path = '/' + str(urllib.quote(unique_name, safe='/~+'))
196 if suffix:
197 path = os.path.join(path, suffix)
198 return str(URI(scheme=scheme, host=host, path=path))
173199
=== modified file 'lib/lp/code/model/branch.py'
--- lib/lp/code/model/branch.py 2010-08-27 02:11:36 +0000
+++ lib/lp/code/model/branch.py 2010-09-03 04:25:59 +0000
@@ -7,15 +7,12 @@
7__all__ = [7__all__ = [
8 'Branch',8 'Branch',
9 'BranchSet',9 'BranchSet',
10 'compose_public_url',
11 ]10 ]
1211
13from datetime import datetime12from datetime import datetime
14import os.path
1513
16from bzrlib import urlutils14from bzrlib import urlutils
17from bzrlib.revision import NULL_REVISION15from bzrlib.revision import NULL_REVISION
18from lazr.uri import URI
19import pytz16import pytz
20from sqlobject import (17from sqlobject import (
21 BoolCol,18 BoolCol,
@@ -108,6 +105,7 @@
108from lp.code.interfaces.branchnamespace import IBranchNamespacePolicy105from lp.code.interfaces.branchnamespace import IBranchNamespacePolicy
109from lp.code.interfaces.branchpuller import IBranchPuller106from lp.code.interfaces.branchpuller import IBranchPuller
110from lp.code.interfaces.branchtarget import IBranchTarget107from lp.code.interfaces.branchtarget import IBranchTarget
108from lp.code.interfaces.codehosting import compose_public_url
111from lp.code.interfaces.seriessourcepackagebranch import (109from lp.code.interfaces.seriessourcepackagebranch import (
112 IFindOfficialBranchLinks,110 IFindOfficialBranchLinks,
113 )111 )
@@ -1324,18 +1322,3 @@
1324 """1322 """
1325 update_trigger_modified_fields(branch)1323 update_trigger_modified_fields(branch)
1326 send_branch_modified_notifications(branch, event)1324 send_branch_modified_notifications(branch, event)
1327
1328
1329def compose_public_url(scheme, unique_name, suffix=None):
1330 # Avoid circular imports.
1331 from lp.code.xmlrpc.branch import PublicCodehostingAPI
1332
1333 # Accept sftp as a legacy protocol.
1334 accepted_schemes = set(PublicCodehostingAPI.supported_schemes)
1335 accepted_schemes.add('sftp')
1336 assert scheme in accepted_schemes, "Unknown scheme: %s" % scheme
1337 host = URI(config.codehosting.supermirror_root).host
1338 path = '/' + urlutils.escape(unique_name)
1339 if suffix:
1340 path = os.path.join(path, suffix)
1341 return str(URI(scheme=scheme, host=host, path=path))
13421325
=== modified file 'lib/lp/code/xmlrpc/branch.py'
--- lib/lp/code/xmlrpc/branch.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/xmlrpc/branch.py 2010-09-03 04:25:59 +0000
@@ -8,8 +8,11 @@
88
9__metaclass__ = type9__metaclass__ = type
10__all__ = [10__all__ = [
11 'BranchSetAPI', 'IBranchSetAPI', 'IPublicCodehostingAPI',11 'BranchSetAPI',
12 'PublicCodehostingAPI']12 'IBranchSetAPI',
13 'IPublicCodehostingAPI',
14 'PublicCodehostingAPI',
15 ]
1316
1417
15from bzrlib import urlutils18from bzrlib import urlutils
@@ -42,6 +45,11 @@
42from lp.code.interfaces.branch import IBranch45from lp.code.interfaces.branch import IBranch
43from lp.code.interfaces.branchlookup import IBranchLookup46from lp.code.interfaces.branchlookup import IBranchLookup
44from lp.code.interfaces.branchnamespace import get_branch_namespace47from lp.code.interfaces.branchnamespace import get_branch_namespace
48from lp.code.interfaces.codehosting import (
49 BRANCH_ALIAS_PREFIX,
50 compose_public_url,
51 SUPPORTED_SCHEMES,
52 )
45from lp.registry.interfaces.distroseries import NoSuchDistroSeries53from lp.registry.interfaces.distroseries import NoSuchDistroSeries
46from lp.registry.interfaces.person import (54from lp.registry.interfaces.person import (
47 IPersonSet,55 IPersonSet,
@@ -171,8 +179,7 @@
171 def resolve_lp_path(path):179 def resolve_lp_path(path):
172 """Expand the path segment of an lp: URL into a list of branch URLs.180 """Expand the path segment of an lp: URL into a list of branch URLs.
173181
174 This method is added to support Bazaar 0.93. It cannot be removed182 This method is added to Bazaar in 0.93.
175 until we stop supporting Bazaar 0.93.
176183
177 :return: A dict containing a single 'urls' key that maps to a list of184 :return: A dict containing a single 'urls' key that maps to a list of
178 URLs. Clients should use the first URL in the list that they can185 URLs. Clients should use the first URL in the list that they can
@@ -194,12 +201,25 @@
194201
195 implements(IPublicCodehostingAPI)202 implements(IPublicCodehostingAPI)
196203
197 supported_schemes = 'bzr+ssh', 'http'204 def _compose_http_url(unique_name, path, suffix):
198205 return compose_public_url('http', unique_name, suffix)
199 def _getResultDict(self, branch, suffix=None, supported_schemes=None):206
207 def _compose_bzr_ssh_url(unique_name, path, suffix):
208 if not path.startswith('~'):
209 path = '%s/%s' % (BRANCH_ALIAS_PREFIX, path)
210 return compose_public_url('bzr+ssh', path, suffix)
211
212 scheme_funcs = {
213 'bzr+ssh': _compose_bzr_ssh_url,
214 'http': _compose_http_url,
215 }
216
217 def _getResultDict(self, branch, lp_path, suffix=None,
218 supported_schemes=None):
200 """Return a result dict with a list of URLs for the given branch.219 """Return a result dict with a list of URLs for the given branch.
201220
202 :param branch: A Branch object.221 :param branch: A Branch object.
222 :param lp_path: The path that was used to traverse to the branch.
203 :param suffix: The section of the path that follows the branch223 :param suffix: The section of the path that follows the branch
204 specification.224 specification.
205 :return: {'urls': [list_of_branch_urls]}.225 :return: {'urls': [list_of_branch_urls]}.
@@ -210,17 +230,18 @@
210 return dict(urls=[branch.url])230 return dict(urls=[branch.url])
211 else:231 else:
212 return self._getUniqueNameResultDict(232 return self._getUniqueNameResultDict(
213 branch.unique_name, suffix, supported_schemes)233 branch.unique_name, suffix, supported_schemes, lp_path)
214234
215 def _getUniqueNameResultDict(self, unique_name, suffix=None,235 def _getUniqueNameResultDict(self, unique_name, suffix=None,
216 supported_schemes=None):236 supported_schemes=None, path=None):
217 from lp.code.model.branch import compose_public_url
218 if supported_schemes is None:237 if supported_schemes is None:
219 supported_schemes = self.supported_schemes238 supported_schemes = SUPPORTED_SCHEMES
239 if path is None:
240 path = unique_name
220 result = dict(urls=[])241 result = dict(urls=[])
221 for scheme in supported_schemes:242 for scheme in supported_schemes:
222 result['urls'].append(243 result['urls'].append(
223 compose_public_url(scheme, unique_name, suffix))244 self.scheme_funcs[scheme](unique_name, path, suffix))
224 return result245 return result
225246
226 @return_fault247 @return_fault
@@ -233,7 +254,7 @@
233 strip_path = path.strip('/')254 strip_path = path.strip('/')
234 if strip_path == '':255 if strip_path == '':
235 raise faults.InvalidBranchIdentifier(path)256 raise faults.InvalidBranchIdentifier(path)
236 supported_schemes = self.supported_schemes257 supported_schemes = SUPPORTED_SCHEMES
237 hot_products = [product.strip() for product258 hot_products = [product.strip() for product
238 in config.codehosting.hot_products.split(',')]259 in config.codehosting.hot_products.split(',')]
239 if strip_path in hot_products:260 if strip_path in hot_products:
@@ -270,7 +291,14 @@
270 raise faults.CannotHaveLinkedBranch(e.component)291 raise faults.CannotHaveLinkedBranch(e.component)
271 except InvalidNamespace, e:292 except InvalidNamespace, e:
272 raise faults.InvalidBranchUniqueName(urlutils.escape(e.name))293 raise faults.InvalidBranchUniqueName(urlutils.escape(e.name))
273 return self._getResultDict(branch, suffix, supported_schemes)294 # Reverse engineer the actual lp_path that is used, so we need to
295 # remove any suffix that may be there from the strip_path.
296 lp_path = strip_path
297 if suffix is not None:
298 # E.g. 'project/trunk/filename.txt' the suffix is 'filename.txt'
299 # we want lp_path to be 'project/trunk'.
300 lp_path = lp_path[:-(len(suffix)+1)]
301 return self._getResultDict(branch, lp_path, suffix, supported_schemes)
274302
275 def resolve_lp_path(self, path):303 def resolve_lp_path(self, path):
276 """See `IPublicCodehostingAPI`."""304 """See `IPublicCodehostingAPI`."""
277305
=== modified file 'lib/lp/code/xmlrpc/tests/test_branch.py'
--- lib/lp/code/xmlrpc/tests/test_branch.py 2010-08-20 20:31:18 +0000
+++ lib/lp/code/xmlrpc/tests/test_branch.py 2010-09-03 04:25:59 +0000
@@ -3,6 +3,8 @@
33
4"""Unit tests for the public codehosting API."""4"""Unit tests for the public codehosting API."""
55
6from __future__ import with_statement
7
6__metaclass__ = type8__metaclass__ = type
7__all__ = []9__all__ = []
810
@@ -15,17 +17,14 @@
15from lazr.uri import URI17from lazr.uri import URI
16from zope.security.proxy import removeSecurityProxy18from zope.security.proxy import removeSecurityProxy
1719
18from canonical.config import config
19from canonical.launchpad.ftests import (
20 login,
21 logout,
22 )
23from canonical.launchpad.xmlrpc import faults20from canonical.launchpad.xmlrpc import faults
24from canonical.testing import DatabaseFunctionalLayer21from canonical.testing import DatabaseFunctionalLayer
25from lp.code.enums import BranchType22from lp.code.enums import BranchType
23from lp.code.interfaces.codehosting import BRANCH_ALIAS_PREFIX
24from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
26from lp.code.xmlrpc.branch import PublicCodehostingAPI25from lp.code.xmlrpc.branch import PublicCodehostingAPI
27from lp.services.xmlrpc import LaunchpadFault26from lp.services.xmlrpc import LaunchpadFault
28from lp.testing import TestCaseWithFactory27from lp.testing import person_logged_in, TestCaseWithFactory
2928
3029
31NON_ASCII_NAME = u'nam\N{LATIN SMALL LETTER E WITH ACUTE}'30NON_ASCII_NAME = u'nam\N{LATIN SMALL LETTER E WITH ACUTE}'
@@ -36,43 +35,45 @@
3635
37 layer = DatabaseFunctionalLayer36 layer = DatabaseFunctionalLayer
3837
39 def setUp(self):38 def makeProdutWithTrunk(self):
40 """Set up the fixture for these unit tests.39 """Make a new project with a trunk hosted branch."""
4140 product = self.factory.makeProduct()
42 - 'project' is an arbitrary Launchpad project.41 # BranchType is only signficiant insofar as it is not a REMOTE branch.
43 - 'trunk' is a branch on 'project', associated with the development42 trunk = self.factory.makeProductBranch(
44 focus.43 branch_type=BranchType.HOSTED, product=product)
44 with person_logged_in(product.owner):
45 ICanHasLinkedBranch(product).setBranch(trunk)
46 return product, trunk
47
48 def assertResolves(self, lp_url_path, public_branch_path, lp_path=None):
49 """Assert that `lp_url_path` resolves to the specified paths.
50
51 :param public_branch_path: The path that is accessible over http.
52 :param lp_path: The short branch alias that will be resolved over
53 bzr+ssh. The branch alias prefix is prefixed to this path.
54 If it is not set, the bzr+ssh resolved name will be checked
55 against the public_branch_path instead.
45 """56 """
46 TestCaseWithFactory.setUp(self)57 api = PublicCodehostingAPI(None, None)
47 self.api = PublicCodehostingAPI(None, None)58 results = api.resolve_lp_path(lp_url_path)
48 self.product = self.factory.makeProduct()59 if lp_path is None:
49 # Associate 'trunk' with the product's development focus. Use60 ssh_branch_path = public_branch_path
50 # removeSecurityProxy so that we can assign directly to branch.61 else:
51 trunk_series = removeSecurityProxy(self.product).development_focus62 ssh_branch_path = '%s/%s' % (BRANCH_ALIAS_PREFIX, lp_path)
52 # BranchType is only signficiant insofar as it is not a REMOTE branch.
53 trunk_series.branch = (
54 self.factory.makeProductBranch(
55 branch_type=BranchType.HOSTED, product=self.product))
56
57 def makePrivateBranch(self, **kwargs):
58 """Create an arbitrary private branch using `makeBranch`."""
59 branch = self.factory.makeAnyBranch(**kwargs)
60 naked_branch = removeSecurityProxy(branch)
61 naked_branch.private = True
62 return branch
63
64 def assertResolves(self, lp_url_path, unique_name):
65 """Assert that `lp_url_path` path expands to `unique_name`."""
66 results = self.api.resolve_lp_path(lp_url_path)
67 # This improves the error message if results happens to be a fault.63 # This improves the error message if results happens to be a fault.
68 if isinstance(results, LaunchpadFault):64 if isinstance(results, LaunchpadFault):
69 raise results65 raise results
70 for url in results['urls']:66 for url in results['urls']:
71 self.assertEqual('/' + unique_name, URI(url).path)67 uri = URI(url)
68 if uri.scheme == 'http':
69 self.assertEqual('/' + public_branch_path, uri.path)
70 else:
71 self.assertEqual('/' + ssh_branch_path, uri.path)
7272
73 def assertFault(self, lp_url_path, expected_fault):73 def assertFault(self, lp_url_path, expected_fault):
74 """Trying to resolve lp_url_path raises the expected fault."""74 """Trying to resolve lp_url_path raises the expected fault."""
75 fault = self.api.resolve_lp_path(lp_url_path)75 api = PublicCodehostingAPI(None, None)
76 fault = api.resolve_lp_path(lp_url_path)
76 self.assertTrue(77 self.assertTrue(
77 isinstance(fault, xmlrpclib.Fault),78 isinstance(fault, xmlrpclib.Fault),
78 "resolve_lp_path(%r) returned %r, not a Fault."79 "resolve_lp_path(%r) returned %r, not a Fault."
@@ -87,35 +88,39 @@
87 # containing a list of these URLs, with the faster and more featureful88 # containing a list of these URLs, with the faster and more featureful
88 # URLs earlier in the list. We use a dict so we can easily add more89 # URLs earlier in the list. We use a dict so we can easily add more
89 # information in the future.90 # information in the future.
90 trunk = self.product.development_focus.branch91 product, trunk = self.makeProdutWithTrunk()
91 results = self.api.resolve_lp_path(self.product.name)92 api = PublicCodehostingAPI(None, None)
93 results = api.resolve_lp_path(product.name)
92 urls = [94 urls = [
93 'bzr+ssh://bazaar.launchpad.dev/%s' % trunk.unique_name,95 'bzr+ssh://bazaar.launchpad.dev/+branch/%s' % product.name,
94 'http://bazaar.launchpad.dev/%s' % trunk.unique_name]96 'http://bazaar.launchpad.dev/%s' % trunk.unique_name]
95 self.assertEqual(dict(urls=urls), results)97 self.assertEqual(dict(urls=urls), results)
9698
97 def test_resultDictForHotProduct(self):99 def test_resultDictForHotProduct(self):
98 # If 'project-name' is in the config.codehosting.hot_products list,100 # If 'project-name' is in the config.codehosting.hot_products list,
99 # lp:project-name will only resolve to the http url.101 # lp:project-name will only resolve to the http url.
100 config.push(102 product, trunk = self.makeProdutWithTrunk()
101 'hot_product',103 self.pushConfig('codehosting', hot_products=product.name)
102 '[codehosting]\nhot_products: %s '% self.product.name)104 api = PublicCodehostingAPI(None, None)
103 self.addCleanup(config.pop, 'hot_product')105 results = api.resolve_lp_path(product.name)
104 trunk = self.product.development_focus.branch
105 results = self.api.resolve_lp_path(self.product.name)
106 http_url = 'http://bazaar.launchpad.dev/%s' % trunk.unique_name106 http_url = 'http://bazaar.launchpad.dev/%s' % trunk.unique_name
107 self.assertEqual(dict(urls=[http_url]), results)107 self.assertEqual(dict(urls=[http_url]), results)
108108
109 def test_product_only(self):109 def test_product_only(self):
110 # lp:product expands to the branch associated with development focus110 # lp:product expands to the branch associated with development focus
111 # of the product.111 # of the product for the anonymous public access, just to the aliased
112 trunk = self.product.development_focus.branch112 # short name for bzr+ssh access.
113 self.assertResolves(self.product.name, trunk.unique_name)113 product, trunk = self.makeProdutWithTrunk()
114 trunk_series = removeSecurityProxy(self.product).development_focus114 lp_path = product.name
115 trunk_series.branch = self.factory.makeProductBranch(115 self.assertResolves(lp_path, trunk.unique_name, lp_path)
116 branch_type=BranchType.HOSTED, product=self.product)116
117 self.assertResolves(117 def test_product_explicit_dev_series(self):
118 self.product.name, trunk_series.branch.unique_name)118 # lp:product/development_focus expands to the branch associated with
119 # development focus of the product for the anonymous public access,
120 # just to the aliased short name for bzr+ssh access.
121 product, trunk = self.makeProdutWithTrunk()
122 lp_path='%s/%s' % (product.name, product.development_focus.name)
123 self.assertResolves(lp_path, trunk.unique_name, lp_path)
119124
120 def test_product_doesnt_exist(self):125 def test_product_doesnt_exist(self):
121 # Return a NoSuchProduct fault if the product doesn't exist.126 # Return a NoSuchProduct fault if the product doesn't exist.
@@ -165,18 +170,11 @@
165 def test_product_and_series(self):170 def test_product_and_series(self):
166 # lp:product/series expands to the branch associated with the product171 # lp:product/series expands to the branch associated with the product
167 # series 'series' on 'product'.172 # series 'series' on 'product'.
168 series = self.factory.makeSeries(173 product = self.factory.makeProduct()
169 product=self.product,174 branch = branch=self.factory.makeProductBranch(product=product)
170 branch=self.factory.makeProductBranch(product=self.product))175 series = self.factory.makeSeries(product=product, branch=branch)
171 self.assertResolves(176 lp_path = '%s/%s' % (product.name, series.name)
172 '%s/%s' % (self.product.name, series.name),177 self.assertResolves(lp_path, branch.unique_name, lp_path)
173 series.branch.unique_name)
174
175 # We can also use product/series notation to reach trunk.
176 self.assertResolves(
177 '%s/%s' % (self.product.name,
178 self.product.development_focus.name),
179 self.product.development_focus.branch.unique_name)
180178
181 def test_development_focus_has_no_branch(self):179 def test_development_focus_has_no_branch(self):
182 # Return a NoLinkedBranch fault if the development focus has no branch180 # Return a NoLinkedBranch fault if the development focus has no branch
@@ -196,17 +194,19 @@
196 def test_no_such_product_series(self):194 def test_no_such_product_series(self):
197 # Return a NoSuchProductSeries fault if there is no series of the195 # Return a NoSuchProductSeries fault if there is no series of the
198 # given name associated with the product.196 # given name associated with the product.
197 product = self.factory.makeProduct()
199 self.assertFault(198 self.assertFault(
200 '%s/%s' % (self.product.name, "doesntexist"),199 '%s/%s' % (product.name, "doesntexist"),
201 faults.NoSuchProductSeries("doesntexist", self.product))200 faults.NoSuchProductSeries("doesntexist", product))
202201
203 def test_no_such_product_series_non_ascii(self):202 def test_no_such_product_series_non_ascii(self):
204 # lp:product/<non-ascii-string> returns NoSuchProductSeries with the203 # lp:product/<non-ascii-string> returns NoSuchProductSeries with the
205 # name escaped.204 # name escaped.
205 product = self.factory.makeProduct()
206 self.assertFault(206 self.assertFault(
207 '%s/%s' % (self.product.name, NON_ASCII_NAME),207 '%s/%s' % (product.name, NON_ASCII_NAME),
208 faults.NoSuchProductSeries(208 faults.NoSuchProductSeries(
209 urlutils.escape(NON_ASCII_NAME), self.product))209 urlutils.escape(NON_ASCII_NAME), product))
210210
211 def test_no_such_distro_series(self):211 def test_no_such_distro_series(self):
212 # Return a NoSuchDistroSeries fault if there is no series of the given212 # Return a NoSuchDistroSeries fault if there is no series of the given
@@ -254,34 +254,37 @@
254 def test_branch(self):254 def test_branch(self):
255 # The unique name of a branch resolves to the unique name of the255 # The unique name of a branch resolves to the unique name of the
256 # branch.256 # branch.
257 arbitrary_branch = self.factory.makeAnyBranch()257 branch = self.factory.makeAnyBranch()
258 self.assertResolves(258 self.assertResolves(branch.unique_name, branch.unique_name)
259 arbitrary_branch.unique_name, arbitrary_branch.unique_name)259
260 trunk = self.product.development_focus.branch260 def test_trunk_accessed_as_branch(self):
261 # A branch that is the development focus for any product can also be
262 # accessed through the branch's unique_name.
263 _ignored, trunk = self.makeProdutWithTrunk()
261 self.assertResolves(trunk.unique_name, trunk.unique_name)264 self.assertResolves(trunk.unique_name, trunk.unique_name)
262265
263 def test_mirrored_branch(self):266 def test_mirrored_branch(self):
264 # The unique name of a mirrored branch resolves to the unique name of267 # The unique name of a mirrored branch resolves to the unique name of
265 # the branch.268 # the branch.
266 arbitrary_branch = self.factory.makeAnyBranch(269 branch = self.factory.makeAnyBranch(branch_type=BranchType.MIRRORED)
267 branch_type=BranchType.MIRRORED)270 self.assertResolves(branch.unique_name, branch.unique_name)
268 self.assertResolves(
269 arbitrary_branch.unique_name, arbitrary_branch.unique_name)
270271
271 def test_no_such_branch_product(self):272 def test_no_such_branch_product(self):
272 # Resolve paths to branches even if there is no branch of that name.273 # Resolve paths to branches even if there is no branch of that name.
273 # We do this so that users can push new branches to lp: URLs.274 # We do this so that users can push new branches to lp: URLs.
274 owner = self.factory.makePerson()275 owner = self.factory.makePerson()
276 product = self.factory.makeProduct()
275 nonexistent_branch = '~%s/%s/doesntexist' % (277 nonexistent_branch = '~%s/%s/doesntexist' % (
276 owner.name, self.product.name)278 owner.name, product.name)
277 self.assertResolves(nonexistent_branch, nonexistent_branch)279 self.assertResolves(nonexistent_branch, nonexistent_branch)
278280
279 def test_no_such_branch_product_non_ascii(self):281 def test_no_such_branch_product_non_ascii(self):
280 # A path to a branch that contains non ascii characters will never282 # A path to a branch that contains non ascii characters will never
281 # find a branch, but it still resolves rather than erroring.283 # find a branch, but it still resolves rather than erroring.
282 owner = self.factory.makePerson()284 owner = self.factory.makePerson()
285 product = self.factory.makeProduct()
283 nonexistent_branch = u'~%s/%s/%s' % (286 nonexistent_branch = u'~%s/%s/%s' % (
284 owner.name, self.product.name, NON_ASCII_NAME)287 owner.name, product.name, NON_ASCII_NAME)
285 self.assertResolves(288 self.assertResolves(
286 nonexistent_branch, urlutils.escape(nonexistent_branch))289 nonexistent_branch, urlutils.escape(nonexistent_branch))
287290
@@ -291,8 +294,7 @@
291 # the '+junk' project, which doesn't actually exist.294 # the '+junk' project, which doesn't actually exist.
292 owner = self.factory.makePerson()295 owner = self.factory.makePerson()
293 nonexistent_branch = '~%s/+junk/doesntexist' % owner.name296 nonexistent_branch = '~%s/+junk/doesntexist' % owner.name
294 self.assertResolves(297 self.assertResolves(nonexistent_branch, nonexistent_branch)
295 nonexistent_branch, urlutils.escape(nonexistent_branch))
296298
297 def test_no_such_branch_package(self):299 def test_no_such_branch_package(self):
298 # Resolve paths to package branches even if there's no branch of that300 # Resolve paths to package branches even if there's no branch of that
@@ -374,26 +376,26 @@
374 def test_trailing_slashes(self):376 def test_trailing_slashes(self):
375 # Trailing slashes are trimmed.377 # Trailing slashes are trimmed.
376 # Trailing slashes on lp:product//378 # Trailing slashes on lp:product//
377 trunk = self.product.development_focus.branch379 product, trunk = self.makeProdutWithTrunk()
378 self.assertResolves(self.product.name + '/', trunk.unique_name)380 self.assertResolves(
379 self.assertResolves(self.product.name + '//', trunk.unique_name)381 product.name + '/', trunk.unique_name, product.name)
382 self.assertResolves(
383 product.name + '//', trunk.unique_name, product.name)
380384
381 # Trailing slashes on lp:~owner/product/branch//385 # Trailing slashes on lp:~owner/product/branch//
382 arbitrary_branch = self.factory.makeAnyBranch()386 branch = self.factory.makeAnyBranch()
383 self.assertResolves(387 self.assertResolves(branch.unique_name + '/', branch.unique_name)
384 arbitrary_branch.unique_name + '/', arbitrary_branch.unique_name)388 self.assertResolves(branch.unique_name + '//', branch.unique_name)
385 self.assertResolves(
386 arbitrary_branch.unique_name + '//', arbitrary_branch.unique_name)
387389
388 def test_private_branch(self):390 def test_private_branch(self):
389 # Invisible branches are resolved as if they didn't exist, so that we391 # Invisible branches are resolved as if they didn't exist, so that we
390 # reveal the least possile amount of information about them.392 # reveal the least possile amount of information about them.
391 # For fully specified branch names, this means resolving the lp url.393 # For fully specified branch names, this means resolving the lp url.
392 arbitrary_branch = self.makePrivateBranch()394 branch = self.factory.makeAnyBranch(private=True)
393 # Removing security proxy to get at the unique_name attribute of a395 # Removing security proxy to get at the unique_name attribute of a
394 # private branch, and tests are currently running as an anonymous396 # private branch, and tests are currently running as an anonymous
395 # user.397 # user.
396 unique_name = removeSecurityProxy(arbitrary_branch).unique_name398 unique_name = removeSecurityProxy(branch).unique_name
397 self.assertResolves(unique_name, unique_name)399 self.assertResolves(unique_name, unique_name)
398400
399 def test_private_branch_on_series(self):401 def test_private_branch_on_series(self):
@@ -404,7 +406,7 @@
404 # Removing security proxy because we need to be able to get at406 # Removing security proxy because we need to be able to get at
405 # attributes of a private branch and these tests are running as an407 # attributes of a private branch and these tests are running as an
406 # anonymous user.408 # anonymous user.
407 branch = removeSecurityProxy(self.makePrivateBranch())409 branch = self.factory.makeAnyBranch(private=True)
408 series = self.factory.makeSeries(branch=branch)410 series = self.factory.makeSeries(branch=branch)
409 self.assertFault(411 self.assertFault(
410 '%s/%s' % (series.product.name, series.name),412 '%s/%s' % (series.product.name, series.name),
@@ -417,11 +419,9 @@
417 # development focus. If that branch is private, other views will419 # development focus. If that branch is private, other views will
418 # indicate that there is no branch on the development focus. We do the420 # indicate that there is no branch on the development focus. We do the
419 # same.421 # same.
420 trunk = self.product.development_focus.branch422 product, trunk = self.makeProdutWithTrunk()
421 naked_trunk = removeSecurityProxy(trunk)423 removeSecurityProxy(trunk).private = True
422 naked_trunk.private = True424 self.assertFault(product.name, faults.NoLinkedBranch(product))
423 self.assertFault(
424 self.product.name, faults.NoLinkedBranch(self.product))
425425
426 def test_private_branch_as_user(self):426 def test_private_branch_as_user(self):
427 # We resolve invisible branches as if they don't exist.427 # We resolve invisible branches as if they don't exist.
@@ -433,19 +433,17 @@
433 #433 #
434 # Create the owner explicitly so that we can get its email without434 # Create the owner explicitly so that we can get its email without
435 # resorting to removeSecurityProxy.435 # resorting to removeSecurityProxy.
436 email = self.factory.getUniqueEmailAddress()436 owner = self.factory.makePerson()
437 arbitrary_branch = self.makePrivateBranch(437 branch = self.factory.makeAnyBranch(owner=owner, private=True)
438 owner=self.factory.makePerson(email=email))438 path = removeSecurityProxy(branch).unique_name
439 login(email)439 self.assertResolves(path, path)
440 self.addCleanup(logout)
441 self.assertResolves(
442 arbitrary_branch.unique_name, arbitrary_branch.unique_name)
443440
444 def test_remote_branch(self):441 def test_remote_branch(self):
445 # For remote branches, return results that link to the actual remote442 # For remote branches, return results that link to the actual remote
446 # branch URL.443 # branch URL.
447 branch = self.factory.makeAnyBranch(branch_type=BranchType.REMOTE)444 branch = self.factory.makeAnyBranch(branch_type=BranchType.REMOTE)
448 result = self.api.resolve_lp_path(branch.unique_name)445 api = PublicCodehostingAPI(None, None)
446 result = api.resolve_lp_path(branch.unique_name)
449 self.assertEqual([branch.url], result['urls'])447 self.assertEqual([branch.url], result['urls'])
450448
451 def test_remote_branch_no_url(self):449 def test_remote_branch_no_url(self):