Merge lp:~thumper/launchpad/xmlrpc-lp-name-resolution into lp:launchpad
- xmlrpc-lp-name-resolution
- Merge into devel
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 |
Related bugs: |
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:
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.
Most of the changes are updates to the tests.
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
1 | === modified file 'lib/lp/code/interfaces/codehosting.py' | |||
2 | --- lib/lp/code/interfaces/codehosting.py 2010-08-20 20:31:18 +0000 | |||
3 | +++ lib/lp/code/interfaces/codehosting.py 2010-09-03 04:25:59 +0000 | |||
4 | @@ -9,17 +9,24 @@ | |||
5 | 9 | __all__ = [ | 9 | __all__ = [ |
6 | 10 | 'BRANCH_ALIAS_PREFIX', | 10 | 'BRANCH_ALIAS_PREFIX', |
7 | 11 | 'BRANCH_TRANSPORT', | 11 | 'BRANCH_TRANSPORT', |
8 | 12 | 'compose_public_url', | ||
9 | 12 | 'CONTROL_TRANSPORT', | 13 | 'CONTROL_TRANSPORT', |
10 | 13 | 'ICodehostingAPI', | 14 | 'ICodehostingAPI', |
11 | 14 | 'ICodehostingApplication', | 15 | 'ICodehostingApplication', |
12 | 15 | 'LAUNCHPAD_ANONYMOUS', | 16 | 'LAUNCHPAD_ANONYMOUS', |
13 | 16 | 'LAUNCHPAD_SERVICES', | 17 | 'LAUNCHPAD_SERVICES', |
14 | 17 | 'READ_ONLY', | 18 | 'READ_ONLY', |
15 | 19 | 'SUPPORTED_SCHEMES', | ||
16 | 18 | 'WRITABLE', | 20 | 'WRITABLE', |
17 | 19 | ] | 21 | ] |
18 | 20 | 22 | ||
19 | 23 | import os.path | ||
20 | 24 | import urllib | ||
21 | 25 | |||
22 | 26 | from lazr.uri import URI | ||
23 | 21 | from zope.interface import Interface | 27 | from zope.interface import Interface |
24 | 22 | 28 | ||
25 | 29 | from canonical.config import config | ||
26 | 23 | from canonical.launchpad.validators.name import valid_name | 30 | from canonical.launchpad.validators.name import valid_name |
27 | 24 | from canonical.launchpad.webapp.interfaces import ILaunchpadApplication | 31 | from canonical.launchpad.webapp.interfaces import ILaunchpadApplication |
28 | 25 | 32 | ||
29 | @@ -49,6 +56,9 @@ | |||
30 | 49 | # The path prefix for getting at branches via their short name. | 56 | # The path prefix for getting at branches via their short name. |
31 | 50 | BRANCH_ALIAS_PREFIX = '+branch' | 57 | BRANCH_ALIAS_PREFIX = '+branch' |
32 | 51 | 58 | ||
33 | 59 | # The scheme types that are supported for codehosting. | ||
34 | 60 | SUPPORTED_SCHEMES = 'bzr+ssh', 'http' | ||
35 | 61 | |||
36 | 52 | 62 | ||
37 | 53 | class ICodehostingApplication(ILaunchpadApplication): | 63 | class ICodehostingApplication(ILaunchpadApplication): |
38 | 54 | """Branch Puller application root.""" | 64 | """Branch Puller application root.""" |
39 | @@ -170,3 +180,19 @@ | |||
40 | 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. |
41 | 171 | (BRANCH_TRANSPORT, {'id': 3, 'writable': False}, '.bzr/README'). | 181 | (BRANCH_TRANSPORT, {'id': 3, 'writable': False}, '.bzr/README'). |
42 | 172 | """ | 182 | """ |
43 | 183 | |||
44 | 184 | |||
45 | 185 | def compose_public_url(scheme, unique_name, suffix=None): | ||
46 | 186 | # Accept sftp as a legacy protocol. | ||
47 | 187 | accepted_schemes = set(SUPPORTED_SCHEMES) | ||
48 | 188 | accepted_schemes.add('sftp') | ||
49 | 189 | assert scheme in accepted_schemes, "Unknown scheme: %s" % scheme | ||
50 | 190 | host = URI(config.codehosting.supermirror_root).host | ||
51 | 191 | if isinstance(unique_name, unicode): | ||
52 | 192 | unique_name = unique_name.encode('utf-8') | ||
53 | 193 | # After quoting and encoding, the path should be perfectly | ||
54 | 194 | # safe as a plain ASCII string, str() just enforces this | ||
55 | 195 | path = '/' + str(urllib.quote(unique_name, safe='/~+')) | ||
56 | 196 | if suffix: | ||
57 | 197 | path = os.path.join(path, suffix) | ||
58 | 198 | return str(URI(scheme=scheme, host=host, path=path)) | ||
59 | 173 | 199 | ||
60 | === modified file 'lib/lp/code/model/branch.py' | |||
61 | --- lib/lp/code/model/branch.py 2010-08-27 02:11:36 +0000 | |||
62 | +++ lib/lp/code/model/branch.py 2010-09-03 04:25:59 +0000 | |||
63 | @@ -7,15 +7,12 @@ | |||
64 | 7 | __all__ = [ | 7 | __all__ = [ |
65 | 8 | 'Branch', | 8 | 'Branch', |
66 | 9 | 'BranchSet', | 9 | 'BranchSet', |
67 | 10 | 'compose_public_url', | ||
68 | 11 | ] | 10 | ] |
69 | 12 | 11 | ||
70 | 13 | from datetime import datetime | 12 | from datetime import datetime |
71 | 14 | import os.path | ||
72 | 15 | 13 | ||
73 | 16 | from bzrlib import urlutils | 14 | from bzrlib import urlutils |
74 | 17 | from bzrlib.revision import NULL_REVISION | 15 | from bzrlib.revision import NULL_REVISION |
75 | 18 | from lazr.uri import URI | ||
76 | 19 | import pytz | 16 | import pytz |
77 | 20 | from sqlobject import ( | 17 | from sqlobject import ( |
78 | 21 | BoolCol, | 18 | BoolCol, |
79 | @@ -108,6 +105,7 @@ | |||
80 | 108 | from lp.code.interfaces.branchnamespace import IBranchNamespacePolicy | 105 | from lp.code.interfaces.branchnamespace import IBranchNamespacePolicy |
81 | 109 | from lp.code.interfaces.branchpuller import IBranchPuller | 106 | from lp.code.interfaces.branchpuller import IBranchPuller |
82 | 110 | from lp.code.interfaces.branchtarget import IBranchTarget | 107 | from lp.code.interfaces.branchtarget import IBranchTarget |
83 | 108 | from lp.code.interfaces.codehosting import compose_public_url | ||
84 | 111 | from lp.code.interfaces.seriessourcepackagebranch import ( | 109 | from lp.code.interfaces.seriessourcepackagebranch import ( |
85 | 112 | IFindOfficialBranchLinks, | 110 | IFindOfficialBranchLinks, |
86 | 113 | ) | 111 | ) |
87 | @@ -1324,18 +1322,3 @@ | |||
88 | 1324 | """ | 1322 | """ |
89 | 1325 | update_trigger_modified_fields(branch) | 1323 | update_trigger_modified_fields(branch) |
90 | 1326 | send_branch_modified_notifications(branch, event) | 1324 | send_branch_modified_notifications(branch, event) |
91 | 1327 | |||
92 | 1328 | |||
93 | 1329 | def compose_public_url(scheme, unique_name, suffix=None): | ||
94 | 1330 | # Avoid circular imports. | ||
95 | 1331 | from lp.code.xmlrpc.branch import PublicCodehostingAPI | ||
96 | 1332 | |||
97 | 1333 | # Accept sftp as a legacy protocol. | ||
98 | 1334 | accepted_schemes = set(PublicCodehostingAPI.supported_schemes) | ||
99 | 1335 | accepted_schemes.add('sftp') | ||
100 | 1336 | assert scheme in accepted_schemes, "Unknown scheme: %s" % scheme | ||
101 | 1337 | host = URI(config.codehosting.supermirror_root).host | ||
102 | 1338 | path = '/' + urlutils.escape(unique_name) | ||
103 | 1339 | if suffix: | ||
104 | 1340 | path = os.path.join(path, suffix) | ||
105 | 1341 | return str(URI(scheme=scheme, host=host, path=path)) | ||
106 | 1342 | 1325 | ||
107 | === modified file 'lib/lp/code/xmlrpc/branch.py' | |||
108 | --- lib/lp/code/xmlrpc/branch.py 2010-08-20 20:31:18 +0000 | |||
109 | +++ lib/lp/code/xmlrpc/branch.py 2010-09-03 04:25:59 +0000 | |||
110 | @@ -8,8 +8,11 @@ | |||
111 | 8 | 8 | ||
112 | 9 | __metaclass__ = type | 9 | __metaclass__ = type |
113 | 10 | __all__ = [ | 10 | __all__ = [ |
116 | 11 | 'BranchSetAPI', 'IBranchSetAPI', 'IPublicCodehostingAPI', | 11 | 'BranchSetAPI', |
117 | 12 | 'PublicCodehostingAPI'] | 12 | 'IBranchSetAPI', |
118 | 13 | 'IPublicCodehostingAPI', | ||
119 | 14 | 'PublicCodehostingAPI', | ||
120 | 15 | ] | ||
121 | 13 | 16 | ||
122 | 14 | 17 | ||
123 | 15 | from bzrlib import urlutils | 18 | from bzrlib import urlutils |
124 | @@ -42,6 +45,11 @@ | |||
125 | 42 | from lp.code.interfaces.branch import IBranch | 45 | from lp.code.interfaces.branch import IBranch |
126 | 43 | from lp.code.interfaces.branchlookup import IBranchLookup | 46 | from lp.code.interfaces.branchlookup import IBranchLookup |
127 | 44 | from lp.code.interfaces.branchnamespace import get_branch_namespace | 47 | from lp.code.interfaces.branchnamespace import get_branch_namespace |
128 | 48 | from lp.code.interfaces.codehosting import ( | ||
129 | 49 | BRANCH_ALIAS_PREFIX, | ||
130 | 50 | compose_public_url, | ||
131 | 51 | SUPPORTED_SCHEMES, | ||
132 | 52 | ) | ||
133 | 45 | from lp.registry.interfaces.distroseries import NoSuchDistroSeries | 53 | from lp.registry.interfaces.distroseries import NoSuchDistroSeries |
134 | 46 | from lp.registry.interfaces.person import ( | 54 | from lp.registry.interfaces.person import ( |
135 | 47 | IPersonSet, | 55 | IPersonSet, |
136 | @@ -171,8 +179,7 @@ | |||
137 | 171 | def resolve_lp_path(path): | 179 | def resolve_lp_path(path): |
138 | 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. |
139 | 173 | 181 | ||
142 | 174 | This method is added to support Bazaar 0.93. It cannot be removed | 182 | This method is added to Bazaar in 0.93. |
141 | 175 | until we stop supporting Bazaar 0.93. | ||
143 | 176 | 183 | ||
144 | 177 | :return: A dict containing a single 'urls' key that maps to a list of | 184 | :return: A dict containing a single 'urls' key that maps to a list of |
145 | 178 | URLs. Clients should use the first URL in the list that they can | 185 | URLs. Clients should use the first URL in the list that they can |
146 | @@ -194,12 +201,25 @@ | |||
147 | 194 | 201 | ||
148 | 195 | implements(IPublicCodehostingAPI) | 202 | implements(IPublicCodehostingAPI) |
149 | 196 | 203 | ||
153 | 197 | supported_schemes = 'bzr+ssh', 'http' | 204 | def _compose_http_url(unique_name, path, suffix): |
154 | 198 | 205 | return compose_public_url('http', unique_name, suffix) | |
155 | 199 | def _getResultDict(self, branch, suffix=None, supported_schemes=None): | 206 | |
156 | 207 | def _compose_bzr_ssh_url(unique_name, path, suffix): | ||
157 | 208 | if not path.startswith('~'): | ||
158 | 209 | path = '%s/%s' % (BRANCH_ALIAS_PREFIX, path) | ||
159 | 210 | return compose_public_url('bzr+ssh', path, suffix) | ||
160 | 211 | |||
161 | 212 | scheme_funcs = { | ||
162 | 213 | 'bzr+ssh': _compose_bzr_ssh_url, | ||
163 | 214 | 'http': _compose_http_url, | ||
164 | 215 | } | ||
165 | 216 | |||
166 | 217 | def _getResultDict(self, branch, lp_path, suffix=None, | ||
167 | 218 | supported_schemes=None): | ||
168 | 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. |
169 | 201 | 220 | ||
170 | 202 | :param branch: A Branch object. | 221 | :param branch: A Branch object. |
171 | 222 | :param lp_path: The path that was used to traverse to the branch. | ||
172 | 203 | :param suffix: The section of the path that follows the branch | 223 | :param suffix: The section of the path that follows the branch |
173 | 204 | specification. | 224 | specification. |
174 | 205 | :return: {'urls': [list_of_branch_urls]}. | 225 | :return: {'urls': [list_of_branch_urls]}. |
175 | @@ -210,17 +230,18 @@ | |||
176 | 210 | return dict(urls=[branch.url]) | 230 | return dict(urls=[branch.url]) |
177 | 211 | else: | 231 | else: |
178 | 212 | return self._getUniqueNameResultDict( | 232 | return self._getUniqueNameResultDict( |
180 | 213 | branch.unique_name, suffix, supported_schemes) | 233 | branch.unique_name, suffix, supported_schemes, lp_path) |
181 | 214 | 234 | ||
182 | 215 | def _getUniqueNameResultDict(self, unique_name, suffix=None, | 235 | def _getUniqueNameResultDict(self, unique_name, suffix=None, |
185 | 216 | supported_schemes=None): | 236 | supported_schemes=None, path=None): |
184 | 217 | from lp.code.model.branch import compose_public_url | ||
186 | 218 | if supported_schemes is None: | 237 | if supported_schemes is None: |
188 | 219 | supported_schemes = self.supported_schemes | 238 | supported_schemes = SUPPORTED_SCHEMES |
189 | 239 | if path is None: | ||
190 | 240 | path = unique_name | ||
191 | 220 | result = dict(urls=[]) | 241 | result = dict(urls=[]) |
192 | 221 | for scheme in supported_schemes: | 242 | for scheme in supported_schemes: |
193 | 222 | result['urls'].append( | 243 | result['urls'].append( |
195 | 223 | compose_public_url(scheme, unique_name, suffix)) | 244 | self.scheme_funcs[scheme](unique_name, path, suffix)) |
196 | 224 | return result | 245 | return result |
197 | 225 | 246 | ||
198 | 226 | @return_fault | 247 | @return_fault |
199 | @@ -233,7 +254,7 @@ | |||
200 | 233 | strip_path = path.strip('/') | 254 | strip_path = path.strip('/') |
201 | 234 | if strip_path == '': | 255 | if strip_path == '': |
202 | 235 | raise faults.InvalidBranchIdentifier(path) | 256 | raise faults.InvalidBranchIdentifier(path) |
204 | 236 | supported_schemes = self.supported_schemes | 257 | supported_schemes = SUPPORTED_SCHEMES |
205 | 237 | hot_products = [product.strip() for product | 258 | hot_products = [product.strip() for product |
206 | 238 | in config.codehosting.hot_products.split(',')] | 259 | in config.codehosting.hot_products.split(',')] |
207 | 239 | if strip_path in hot_products: | 260 | if strip_path in hot_products: |
208 | @@ -270,7 +291,14 @@ | |||
209 | 270 | raise faults.CannotHaveLinkedBranch(e.component) | 291 | raise faults.CannotHaveLinkedBranch(e.component) |
210 | 271 | except InvalidNamespace, e: | 292 | except InvalidNamespace, e: |
211 | 272 | raise faults.InvalidBranchUniqueName(urlutils.escape(e.name)) | 293 | raise faults.InvalidBranchUniqueName(urlutils.escape(e.name)) |
213 | 273 | return self._getResultDict(branch, suffix, supported_schemes) | 294 | # Reverse engineer the actual lp_path that is used, so we need to |
214 | 295 | # remove any suffix that may be there from the strip_path. | ||
215 | 296 | lp_path = strip_path | ||
216 | 297 | if suffix is not None: | ||
217 | 298 | # E.g. 'project/trunk/filename.txt' the suffix is 'filename.txt' | ||
218 | 299 | # we want lp_path to be 'project/trunk'. | ||
219 | 300 | lp_path = lp_path[:-(len(suffix)+1)] | ||
220 | 301 | return self._getResultDict(branch, lp_path, suffix, supported_schemes) | ||
221 | 274 | 302 | ||
222 | 275 | def resolve_lp_path(self, path): | 303 | def resolve_lp_path(self, path): |
223 | 276 | """See `IPublicCodehostingAPI`.""" | 304 | """See `IPublicCodehostingAPI`.""" |
224 | 277 | 305 | ||
225 | === modified file 'lib/lp/code/xmlrpc/tests/test_branch.py' | |||
226 | --- lib/lp/code/xmlrpc/tests/test_branch.py 2010-08-20 20:31:18 +0000 | |||
227 | +++ lib/lp/code/xmlrpc/tests/test_branch.py 2010-09-03 04:25:59 +0000 | |||
228 | @@ -3,6 +3,8 @@ | |||
229 | 3 | 3 | ||
230 | 4 | """Unit tests for the public codehosting API.""" | 4 | """Unit tests for the public codehosting API.""" |
231 | 5 | 5 | ||
232 | 6 | from __future__ import with_statement | ||
233 | 7 | |||
234 | 6 | __metaclass__ = type | 8 | __metaclass__ = type |
235 | 7 | __all__ = [] | 9 | __all__ = [] |
236 | 8 | 10 | ||
237 | @@ -15,17 +17,14 @@ | |||
238 | 15 | from lazr.uri import URI | 17 | from lazr.uri import URI |
239 | 16 | from zope.security.proxy import removeSecurityProxy | 18 | from zope.security.proxy import removeSecurityProxy |
240 | 17 | 19 | ||
241 | 18 | from canonical.config import config | ||
242 | 19 | from canonical.launchpad.ftests import ( | ||
243 | 20 | login, | ||
244 | 21 | logout, | ||
245 | 22 | ) | ||
246 | 23 | from canonical.launchpad.xmlrpc import faults | 20 | from canonical.launchpad.xmlrpc import faults |
247 | 24 | from canonical.testing import DatabaseFunctionalLayer | 21 | from canonical.testing import DatabaseFunctionalLayer |
248 | 25 | from lp.code.enums import BranchType | 22 | from lp.code.enums import BranchType |
249 | 23 | from lp.code.interfaces.codehosting import BRANCH_ALIAS_PREFIX | ||
250 | 24 | from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch | ||
251 | 26 | from lp.code.xmlrpc.branch import PublicCodehostingAPI | 25 | from lp.code.xmlrpc.branch import PublicCodehostingAPI |
252 | 27 | from lp.services.xmlrpc import LaunchpadFault | 26 | from lp.services.xmlrpc import LaunchpadFault |
254 | 28 | from lp.testing import TestCaseWithFactory | 27 | from lp.testing import person_logged_in, TestCaseWithFactory |
255 | 29 | 28 | ||
256 | 30 | 29 | ||
257 | 31 | NON_ASCII_NAME = u'nam\N{LATIN SMALL LETTER E WITH ACUTE}' | 30 | NON_ASCII_NAME = u'nam\N{LATIN SMALL LETTER E WITH ACUTE}' |
258 | @@ -36,43 +35,45 @@ | |||
259 | 36 | 35 | ||
260 | 37 | layer = DatabaseFunctionalLayer | 36 | layer = DatabaseFunctionalLayer |
261 | 38 | 37 | ||
268 | 39 | def setUp(self): | 38 | def makeProdutWithTrunk(self): |
269 | 40 | """Set up the fixture for these unit tests. | 39 | """Make a new project with a trunk hosted branch.""" |
270 | 41 | 40 | product = self.factory.makeProduct() | |
271 | 42 | - 'project' is an arbitrary Launchpad project. | 41 | # BranchType is only signficiant insofar as it is not a REMOTE branch. |
272 | 43 | - 'trunk' is a branch on 'project', associated with the development | 42 | trunk = self.factory.makeProductBranch( |
273 | 44 | focus. | 43 | branch_type=BranchType.HOSTED, product=product) |
274 | 44 | with person_logged_in(product.owner): | ||
275 | 45 | ICanHasLinkedBranch(product).setBranch(trunk) | ||
276 | 46 | return product, trunk | ||
277 | 47 | |||
278 | 48 | def assertResolves(self, lp_url_path, public_branch_path, lp_path=None): | ||
279 | 49 | """Assert that `lp_url_path` resolves to the specified paths. | ||
280 | 50 | |||
281 | 51 | :param public_branch_path: The path that is accessible over http. | ||
282 | 52 | :param lp_path: The short branch alias that will be resolved over | ||
283 | 53 | bzr+ssh. The branch alias prefix is prefixed to this path. | ||
284 | 54 | If it is not set, the bzr+ssh resolved name will be checked | ||
285 | 55 | against the public_branch_path instead. | ||
286 | 45 | """ | 56 | """ |
308 | 46 | TestCaseWithFactory.setUp(self) | 57 | api = PublicCodehostingAPI(None, None) |
309 | 47 | self.api = PublicCodehostingAPI(None, None) | 58 | results = api.resolve_lp_path(lp_url_path) |
310 | 48 | self.product = self.factory.makeProduct() | 59 | if lp_path is None: |
311 | 49 | # Associate 'trunk' with the product's development focus. Use | 60 | ssh_branch_path = public_branch_path |
312 | 50 | # removeSecurityProxy so that we can assign directly to branch. | 61 | else: |
313 | 51 | trunk_series = removeSecurityProxy(self.product).development_focus | 62 | ssh_branch_path = '%s/%s' % (BRANCH_ALIAS_PREFIX, lp_path) |
293 | 52 | # BranchType is only signficiant insofar as it is not a REMOTE branch. | ||
294 | 53 | trunk_series.branch = ( | ||
295 | 54 | self.factory.makeProductBranch( | ||
296 | 55 | branch_type=BranchType.HOSTED, product=self.product)) | ||
297 | 56 | |||
298 | 57 | def makePrivateBranch(self, **kwargs): | ||
299 | 58 | """Create an arbitrary private branch using `makeBranch`.""" | ||
300 | 59 | branch = self.factory.makeAnyBranch(**kwargs) | ||
301 | 60 | naked_branch = removeSecurityProxy(branch) | ||
302 | 61 | naked_branch.private = True | ||
303 | 62 | return branch | ||
304 | 63 | |||
305 | 64 | def assertResolves(self, lp_url_path, unique_name): | ||
306 | 65 | """Assert that `lp_url_path` path expands to `unique_name`.""" | ||
307 | 66 | results = self.api.resolve_lp_path(lp_url_path) | ||
314 | 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. |
315 | 68 | if isinstance(results, LaunchpadFault): | 64 | if isinstance(results, LaunchpadFault): |
316 | 69 | raise results | 65 | raise results |
317 | 70 | for url in results['urls']: | 66 | for url in results['urls']: |
319 | 71 | self.assertEqual('/' + unique_name, URI(url).path) | 67 | uri = URI(url) |
320 | 68 | if uri.scheme == 'http': | ||
321 | 69 | self.assertEqual('/' + public_branch_path, uri.path) | ||
322 | 70 | else: | ||
323 | 71 | self.assertEqual('/' + ssh_branch_path, uri.path) | ||
324 | 72 | 72 | ||
325 | 73 | def assertFault(self, lp_url_path, expected_fault): | 73 | def assertFault(self, lp_url_path, expected_fault): |
326 | 74 | """Trying to resolve lp_url_path raises the expected fault.""" | 74 | """Trying to resolve lp_url_path raises the expected fault.""" |
328 | 75 | fault = self.api.resolve_lp_path(lp_url_path) | 75 | api = PublicCodehostingAPI(None, None) |
329 | 76 | fault = api.resolve_lp_path(lp_url_path) | ||
330 | 76 | self.assertTrue( | 77 | self.assertTrue( |
331 | 77 | isinstance(fault, xmlrpclib.Fault), | 78 | isinstance(fault, xmlrpclib.Fault), |
332 | 78 | "resolve_lp_path(%r) returned %r, not a Fault." | 79 | "resolve_lp_path(%r) returned %r, not a Fault." |
333 | @@ -87,35 +88,39 @@ | |||
334 | 87 | # containing a list of these URLs, with the faster and more featureful | 88 | # containing a list of these URLs, with the faster and more featureful |
335 | 88 | # URLs earlier in the list. We use a dict so we can easily add more | 89 | # URLs earlier in the list. We use a dict so we can easily add more |
336 | 89 | # information in the future. | 90 | # information in the future. |
339 | 90 | trunk = self.product.development_focus.branch | 91 | product, trunk = self.makeProdutWithTrunk() |
340 | 91 | results = self.api.resolve_lp_path(self.product.name) | 92 | api = PublicCodehostingAPI(None, None) |
341 | 93 | results = api.resolve_lp_path(product.name) | ||
342 | 92 | urls = [ | 94 | urls = [ |
344 | 93 | 'bzr+ssh://bazaar.launchpad.dev/%s' % trunk.unique_name, | 95 | 'bzr+ssh://bazaar.launchpad.dev/+branch/%s' % product.name, |
345 | 94 | 'http://bazaar.launchpad.dev/%s' % trunk.unique_name] | 96 | 'http://bazaar.launchpad.dev/%s' % trunk.unique_name] |
346 | 95 | self.assertEqual(dict(urls=urls), results) | 97 | self.assertEqual(dict(urls=urls), results) |
347 | 96 | 98 | ||
348 | 97 | def test_resultDictForHotProduct(self): | 99 | def test_resultDictForHotProduct(self): |
349 | 98 | # If 'project-name' is in the config.codehosting.hot_products list, | 100 | # If 'project-name' is in the config.codehosting.hot_products list, |
350 | 99 | # lp:project-name will only resolve to the http url. | 101 | # lp:project-name will only resolve to the http url. |
357 | 100 | config.push( | 102 | product, trunk = self.makeProdutWithTrunk() |
358 | 101 | 'hot_product', | 103 | self.pushConfig('codehosting', hot_products=product.name) |
359 | 102 | '[codehosting]\nhot_products: %s '% self.product.name) | 104 | api = PublicCodehostingAPI(None, None) |
360 | 103 | self.addCleanup(config.pop, 'hot_product') | 105 | results = api.resolve_lp_path(product.name) |
355 | 104 | trunk = self.product.development_focus.branch | ||
356 | 105 | results = self.api.resolve_lp_path(self.product.name) | ||
361 | 106 | http_url = 'http://bazaar.launchpad.dev/%s' % trunk.unique_name | 106 | http_url = 'http://bazaar.launchpad.dev/%s' % trunk.unique_name |
362 | 107 | self.assertEqual(dict(urls=[http_url]), results) | 107 | self.assertEqual(dict(urls=[http_url]), results) |
363 | 108 | 108 | ||
364 | 109 | def test_product_only(self): | 109 | def test_product_only(self): |
365 | 110 | # lp:product expands to the branch associated with development focus | 110 | # lp:product expands to the branch associated with development focus |
374 | 111 | # of the product. | 111 | # of the product for the anonymous public access, just to the aliased |
375 | 112 | trunk = self.product.development_focus.branch | 112 | # short name for bzr+ssh access. |
376 | 113 | self.assertResolves(self.product.name, trunk.unique_name) | 113 | product, trunk = self.makeProdutWithTrunk() |
377 | 114 | trunk_series = removeSecurityProxy(self.product).development_focus | 114 | lp_path = product.name |
378 | 115 | trunk_series.branch = self.factory.makeProductBranch( | 115 | self.assertResolves(lp_path, trunk.unique_name, lp_path) |
379 | 116 | branch_type=BranchType.HOSTED, product=self.product) | 116 | |
380 | 117 | self.assertResolves( | 117 | def test_product_explicit_dev_series(self): |
381 | 118 | self.product.name, trunk_series.branch.unique_name) | 118 | # lp:product/development_focus expands to the branch associated with |
382 | 119 | # development focus of the product for the anonymous public access, | ||
383 | 120 | # just to the aliased short name for bzr+ssh access. | ||
384 | 121 | product, trunk = self.makeProdutWithTrunk() | ||
385 | 122 | lp_path='%s/%s' % (product.name, product.development_focus.name) | ||
386 | 123 | self.assertResolves(lp_path, trunk.unique_name, lp_path) | ||
387 | 119 | 124 | ||
388 | 120 | def test_product_doesnt_exist(self): | 125 | def test_product_doesnt_exist(self): |
389 | 121 | # Return a NoSuchProduct fault if the product doesn't exist. | 126 | # Return a NoSuchProduct fault if the product doesn't exist. |
390 | @@ -165,18 +170,11 @@ | |||
391 | 165 | def test_product_and_series(self): | 170 | def test_product_and_series(self): |
392 | 166 | # lp:product/series expands to the branch associated with the product | 171 | # lp:product/series expands to the branch associated with the product |
393 | 167 | # series 'series' on 'product'. | 172 | # series 'series' on 'product'. |
406 | 168 | series = self.factory.makeSeries( | 173 | product = self.factory.makeProduct() |
407 | 169 | product=self.product, | 174 | branch = branch=self.factory.makeProductBranch(product=product) |
408 | 170 | branch=self.factory.makeProductBranch(product=self.product)) | 175 | series = self.factory.makeSeries(product=product, branch=branch) |
409 | 171 | self.assertResolves( | 176 | lp_path = '%s/%s' % (product.name, series.name) |
410 | 172 | '%s/%s' % (self.product.name, series.name), | 177 | self.assertResolves(lp_path, branch.unique_name, lp_path) |
399 | 173 | series.branch.unique_name) | ||
400 | 174 | |||
401 | 175 | # We can also use product/series notation to reach trunk. | ||
402 | 176 | self.assertResolves( | ||
403 | 177 | '%s/%s' % (self.product.name, | ||
404 | 178 | self.product.development_focus.name), | ||
405 | 179 | self.product.development_focus.branch.unique_name) | ||
411 | 180 | 178 | ||
412 | 181 | def test_development_focus_has_no_branch(self): | 179 | def test_development_focus_has_no_branch(self): |
413 | 182 | # Return a NoLinkedBranch fault if the development focus has no branch | 180 | # Return a NoLinkedBranch fault if the development focus has no branch |
414 | @@ -196,17 +194,19 @@ | |||
415 | 196 | def test_no_such_product_series(self): | 194 | def test_no_such_product_series(self): |
416 | 197 | # Return a NoSuchProductSeries fault if there is no series of the | 195 | # Return a NoSuchProductSeries fault if there is no series of the |
417 | 198 | # given name associated with the product. | 196 | # given name associated with the product. |
418 | 197 | product = self.factory.makeProduct() | ||
419 | 199 | self.assertFault( | 198 | self.assertFault( |
422 | 200 | '%s/%s' % (self.product.name, "doesntexist"), | 199 | '%s/%s' % (product.name, "doesntexist"), |
423 | 201 | faults.NoSuchProductSeries("doesntexist", self.product)) | 200 | faults.NoSuchProductSeries("doesntexist", product)) |
424 | 202 | 201 | ||
425 | 203 | def test_no_such_product_series_non_ascii(self): | 202 | def test_no_such_product_series_non_ascii(self): |
426 | 204 | # lp:product/<non-ascii-string> returns NoSuchProductSeries with the | 203 | # lp:product/<non-ascii-string> returns NoSuchProductSeries with the |
427 | 205 | # name escaped. | 204 | # name escaped. |
428 | 205 | product = self.factory.makeProduct() | ||
429 | 206 | self.assertFault( | 206 | self.assertFault( |
431 | 207 | '%s/%s' % (self.product.name, NON_ASCII_NAME), | 207 | '%s/%s' % (product.name, NON_ASCII_NAME), |
432 | 208 | faults.NoSuchProductSeries( | 208 | faults.NoSuchProductSeries( |
434 | 209 | urlutils.escape(NON_ASCII_NAME), self.product)) | 209 | urlutils.escape(NON_ASCII_NAME), product)) |
435 | 210 | 210 | ||
436 | 211 | def test_no_such_distro_series(self): | 211 | def test_no_such_distro_series(self): |
437 | 212 | # Return a NoSuchDistroSeries fault if there is no series of the given | 212 | # Return a NoSuchDistroSeries fault if there is no series of the given |
438 | @@ -254,34 +254,37 @@ | |||
439 | 254 | def test_branch(self): | 254 | def test_branch(self): |
440 | 255 | # The unique name of a branch resolves to the unique name of the | 255 | # The unique name of a branch resolves to the unique name of the |
441 | 256 | # branch. | 256 | # branch. |
446 | 257 | arbitrary_branch = self.factory.makeAnyBranch() | 257 | branch = self.factory.makeAnyBranch() |
447 | 258 | self.assertResolves( | 258 | self.assertResolves(branch.unique_name, branch.unique_name) |
448 | 259 | arbitrary_branch.unique_name, arbitrary_branch.unique_name) | 259 | |
449 | 260 | trunk = self.product.development_focus.branch | 260 | def test_trunk_accessed_as_branch(self): |
450 | 261 | # A branch that is the development focus for any product can also be | ||
451 | 262 | # accessed through the branch's unique_name. | ||
452 | 263 | _ignored, trunk = self.makeProdutWithTrunk() | ||
453 | 261 | self.assertResolves(trunk.unique_name, trunk.unique_name) | 264 | self.assertResolves(trunk.unique_name, trunk.unique_name) |
454 | 262 | 265 | ||
455 | 263 | def test_mirrored_branch(self): | 266 | def test_mirrored_branch(self): |
456 | 264 | # The unique name of a mirrored branch resolves to the unique name of | 267 | # The unique name of a mirrored branch resolves to the unique name of |
457 | 265 | # the branch. | 268 | # the branch. |
462 | 266 | arbitrary_branch = self.factory.makeAnyBranch( | 269 | branch = self.factory.makeAnyBranch(branch_type=BranchType.MIRRORED) |
463 | 267 | branch_type=BranchType.MIRRORED) | 270 | self.assertResolves(branch.unique_name, branch.unique_name) |
460 | 268 | self.assertResolves( | ||
461 | 269 | arbitrary_branch.unique_name, arbitrary_branch.unique_name) | ||
464 | 270 | 271 | ||
465 | 271 | def test_no_such_branch_product(self): | 272 | def test_no_such_branch_product(self): |
466 | 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. |
467 | 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. |
468 | 274 | owner = self.factory.makePerson() | 275 | owner = self.factory.makePerson() |
469 | 276 | product = self.factory.makeProduct() | ||
470 | 275 | nonexistent_branch = '~%s/%s/doesntexist' % ( | 277 | nonexistent_branch = '~%s/%s/doesntexist' % ( |
472 | 276 | owner.name, self.product.name) | 278 | owner.name, product.name) |
473 | 277 | self.assertResolves(nonexistent_branch, nonexistent_branch) | 279 | self.assertResolves(nonexistent_branch, nonexistent_branch) |
474 | 278 | 280 | ||
475 | 279 | def test_no_such_branch_product_non_ascii(self): | 281 | def test_no_such_branch_product_non_ascii(self): |
476 | 280 | # A path to a branch that contains non ascii characters will never | 282 | # A path to a branch that contains non ascii characters will never |
477 | 281 | # find a branch, but it still resolves rather than erroring. | 283 | # find a branch, but it still resolves rather than erroring. |
478 | 282 | owner = self.factory.makePerson() | 284 | owner = self.factory.makePerson() |
479 | 285 | product = self.factory.makeProduct() | ||
480 | 283 | nonexistent_branch = u'~%s/%s/%s' % ( | 286 | nonexistent_branch = u'~%s/%s/%s' % ( |
482 | 284 | owner.name, self.product.name, NON_ASCII_NAME) | 287 | owner.name, product.name, NON_ASCII_NAME) |
483 | 285 | self.assertResolves( | 288 | self.assertResolves( |
484 | 286 | nonexistent_branch, urlutils.escape(nonexistent_branch)) | 289 | nonexistent_branch, urlutils.escape(nonexistent_branch)) |
485 | 287 | 290 | ||
486 | @@ -291,8 +294,7 @@ | |||
487 | 291 | # the '+junk' project, which doesn't actually exist. | 294 | # the '+junk' project, which doesn't actually exist. |
488 | 292 | owner = self.factory.makePerson() | 295 | owner = self.factory.makePerson() |
489 | 293 | nonexistent_branch = '~%s/+junk/doesntexist' % owner.name | 296 | nonexistent_branch = '~%s/+junk/doesntexist' % owner.name |
492 | 294 | self.assertResolves( | 297 | self.assertResolves(nonexistent_branch, nonexistent_branch) |
491 | 295 | nonexistent_branch, urlutils.escape(nonexistent_branch)) | ||
493 | 296 | 298 | ||
494 | 297 | def test_no_such_branch_package(self): | 299 | def test_no_such_branch_package(self): |
495 | 298 | # Resolve paths to package branches even if there's no branch of that | 300 | # Resolve paths to package branches even if there's no branch of that |
496 | @@ -374,26 +376,26 @@ | |||
497 | 374 | def test_trailing_slashes(self): | 376 | def test_trailing_slashes(self): |
498 | 375 | # Trailing slashes are trimmed. | 377 | # Trailing slashes are trimmed. |
499 | 376 | # Trailing slashes on lp:product// | 378 | # Trailing slashes on lp:product// |
503 | 377 | trunk = self.product.development_focus.branch | 379 | product, trunk = self.makeProdutWithTrunk() |
504 | 378 | self.assertResolves(self.product.name + '/', trunk.unique_name) | 380 | self.assertResolves( |
505 | 379 | self.assertResolves(self.product.name + '//', trunk.unique_name) | 381 | product.name + '/', trunk.unique_name, product.name) |
506 | 382 | self.assertResolves( | ||
507 | 383 | product.name + '//', trunk.unique_name, product.name) | ||
508 | 380 | 384 | ||
509 | 381 | # Trailing slashes on lp:~owner/product/branch// | 385 | # Trailing slashes on lp:~owner/product/branch// |
515 | 382 | arbitrary_branch = self.factory.makeAnyBranch() | 386 | branch = self.factory.makeAnyBranch() |
516 | 383 | self.assertResolves( | 387 | self.assertResolves(branch.unique_name + '/', branch.unique_name) |
517 | 384 | arbitrary_branch.unique_name + '/', arbitrary_branch.unique_name) | 388 | self.assertResolves(branch.unique_name + '//', branch.unique_name) |
513 | 385 | self.assertResolves( | ||
514 | 386 | arbitrary_branch.unique_name + '//', arbitrary_branch.unique_name) | ||
518 | 387 | 389 | ||
519 | 388 | def test_private_branch(self): | 390 | def test_private_branch(self): |
520 | 389 | # Invisible branches are resolved as if they didn't exist, so that we | 391 | # Invisible branches are resolved as if they didn't exist, so that we |
521 | 390 | # reveal the least possile amount of information about them. | 392 | # reveal the least possile amount of information about them. |
522 | 391 | # For fully specified branch names, this means resolving the lp url. | 393 | # For fully specified branch names, this means resolving the lp url. |
524 | 392 | arbitrary_branch = self.makePrivateBranch() | 394 | branch = self.factory.makeAnyBranch(private=True) |
525 | 393 | # Removing security proxy to get at the unique_name attribute of a | 395 | # Removing security proxy to get at the unique_name attribute of a |
526 | 394 | # private branch, and tests are currently running as an anonymous | 396 | # private branch, and tests are currently running as an anonymous |
527 | 395 | # user. | 397 | # user. |
529 | 396 | unique_name = removeSecurityProxy(arbitrary_branch).unique_name | 398 | unique_name = removeSecurityProxy(branch).unique_name |
530 | 397 | self.assertResolves(unique_name, unique_name) | 399 | self.assertResolves(unique_name, unique_name) |
531 | 398 | 400 | ||
532 | 399 | def test_private_branch_on_series(self): | 401 | def test_private_branch_on_series(self): |
533 | @@ -404,7 +406,7 @@ | |||
534 | 404 | # Removing security proxy because we need to be able to get at | 406 | # Removing security proxy because we need to be able to get at |
535 | 405 | # attributes of a private branch and these tests are running as an | 407 | # attributes of a private branch and these tests are running as an |
536 | 406 | # anonymous user. | 408 | # anonymous user. |
538 | 407 | branch = removeSecurityProxy(self.makePrivateBranch()) | 409 | branch = self.factory.makeAnyBranch(private=True) |
539 | 408 | series = self.factory.makeSeries(branch=branch) | 410 | series = self.factory.makeSeries(branch=branch) |
540 | 409 | self.assertFault( | 411 | self.assertFault( |
541 | 410 | '%s/%s' % (series.product.name, series.name), | 412 | '%s/%s' % (series.product.name, series.name), |
542 | @@ -417,11 +419,9 @@ | |||
543 | 417 | # development focus. If that branch is private, other views will | 419 | # development focus. If that branch is private, other views will |
544 | 418 | # indicate that there is no branch on the development focus. We do the | 420 | # indicate that there is no branch on the development focus. We do the |
545 | 419 | # same. | 421 | # same. |
551 | 420 | trunk = self.product.development_focus.branch | 422 | product, trunk = self.makeProdutWithTrunk() |
552 | 421 | naked_trunk = removeSecurityProxy(trunk) | 423 | removeSecurityProxy(trunk).private = True |
553 | 422 | naked_trunk.private = True | 424 | self.assertFault(product.name, faults.NoLinkedBranch(product)) |
549 | 423 | self.assertFault( | ||
550 | 424 | self.product.name, faults.NoLinkedBranch(self.product)) | ||
554 | 425 | 425 | ||
555 | 426 | def test_private_branch_as_user(self): | 426 | def test_private_branch_as_user(self): |
556 | 427 | # We resolve invisible branches as if they don't exist. | 427 | # We resolve invisible branches as if they don't exist. |
557 | @@ -433,19 +433,17 @@ | |||
558 | 433 | # | 433 | # |
559 | 434 | # Create the owner explicitly so that we can get its email without | 434 | # Create the owner explicitly so that we can get its email without |
560 | 435 | # resorting to removeSecurityProxy. | 435 | # resorting to removeSecurityProxy. |
568 | 436 | email = self.factory.getUniqueEmailAddress() | 436 | owner = self.factory.makePerson() |
569 | 437 | arbitrary_branch = self.makePrivateBranch( | 437 | branch = self.factory.makeAnyBranch(owner=owner, private=True) |
570 | 438 | owner=self.factory.makePerson(email=email)) | 438 | path = removeSecurityProxy(branch).unique_name |
571 | 439 | login(email) | 439 | self.assertResolves(path, path) |
565 | 440 | self.addCleanup(logout) | ||
566 | 441 | self.assertResolves( | ||
567 | 442 | arbitrary_branch.unique_name, arbitrary_branch.unique_name) | ||
572 | 443 | 440 | ||
573 | 444 | def test_remote_branch(self): | 441 | def test_remote_branch(self): |
574 | 445 | # For remote branches, return results that link to the actual remote | 442 | # For remote branches, return results that link to the actual remote |
575 | 446 | # branch URL. | 443 | # branch URL. |
576 | 447 | branch = self.factory.makeAnyBranch(branch_type=BranchType.REMOTE) | 444 | branch = self.factory.makeAnyBranch(branch_type=BranchType.REMOTE) |
578 | 448 | result = self.api.resolve_lp_path(branch.unique_name) | 445 | api = PublicCodehostingAPI(None, None) |
579 | 446 | result = api.resolve_lp_path(branch.unique_name) | ||
580 | 449 | self.assertEqual([branch.url], result['urls']) | 447 | self.assertEqual([branch.url], result['urls']) |
581 | 450 | 448 | ||
582 | 451 | def test_remote_branch_no_url(self): | 449 | def test_remote_branch_no_url(self): |
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