launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #00852
[Merge] lp:~thumper/launchpad/xmlrpc-lp-name-resolution into lp:launchpad/devel
Tim Penhey has proposed merging lp:~thumper/launchpad/xmlrpc-lp-name-resolution into lp:launchpad/devel.
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
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.
--
https://code.launchpad.net/~thumper/launchpad/xmlrpc-lp-name-resolution/+merge/34500
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~thumper/launchpad/xmlrpc-lp-name-resolution into lp:launchpad/devel.
=== 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-02 22:50:58 +0000
@@ -9,17 +9,24 @@
__all__ = [
'BRANCH_ALIAS_PREFIX',
'BRANCH_TRANSPORT',
+ 'compose_public_url',
'CONTROL_TRANSPORT',
'ICodehostingAPI',
'ICodehostingApplication',
'LAUNCHPAD_ANONYMOUS',
'LAUNCHPAD_SERVICES',
'READ_ONLY',
+ 'SUPPORTED_SCHEMES',
'WRITABLE',
]
+import os.path
+import urllib
+
+from lazr.uri import URI
from zope.interface import Interface
+from canonical.config import config
from canonical.launchpad.validators.name import valid_name
from canonical.launchpad.webapp.interfaces import ILaunchpadApplication
@@ -49,6 +56,10 @@
# The path prefix for getting at branches via their short name.
BRANCH_ALIAS_PREFIX = '+branch'
+# The scheme types that are supported for codehosting.
+SUPPORTED_SCHEMES = 'bzr+ssh', 'http'
+
+
class ICodehostingApplication(ILaunchpadApplication):
"""Branch Puller application root."""
@@ -170,3 +181,19 @@
'path_in_transport' is a path relative to that transport. e.g.
(BRANCH_TRANSPORT, {'id': 3, 'writable': False}, '.bzr/README').
"""
+
+
+def compose_public_url(scheme, unique_name, suffix=None):
+ # Accept sftp as a legacy protocol.
+ accepted_schemes = set(SUPPORTED_SCHEMES)
+ accepted_schemes.add('sftp')
+ assert scheme in accepted_schemes, "Unknown scheme: %s" % scheme
+ host = URI(config.codehosting.supermirror_root).host
+ if isinstance(unique_name, unicode):
+ unique_name = unique_name.encode('utf-8')
+ # After quoting and encoding, the path should be perfectly
+ # safe as a plain ASCII string, str() just enforces this
+ path = '/' + str(urllib.quote(unique_name, safe='/~+'))
+ if suffix:
+ path = os.path.join(path, suffix)
+ return str(URI(scheme=scheme, host=host, path=path))
=== 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-02 22:50:58 +0000
@@ -7,15 +7,12 @@
__all__ = [
'Branch',
'BranchSet',
- 'compose_public_url',
]
from datetime import datetime
-import os.path
from bzrlib import urlutils
from bzrlib.revision import NULL_REVISION
-from lazr.uri import URI
import pytz
from sqlobject import (
BoolCol,
@@ -108,6 +105,7 @@
from lp.code.interfaces.branchnamespace import IBranchNamespacePolicy
from lp.code.interfaces.branchpuller import IBranchPuller
from lp.code.interfaces.branchtarget import IBranchTarget
+from lp.code.interfaces.codehosting import compose_public_url
from lp.code.interfaces.seriessourcepackagebranch import (
IFindOfficialBranchLinks,
)
@@ -1324,18 +1322,3 @@
"""
update_trigger_modified_fields(branch)
send_branch_modified_notifications(branch, event)
-
-
-def compose_public_url(scheme, unique_name, suffix=None):
- # Avoid circular imports.
- from lp.code.xmlrpc.branch import PublicCodehostingAPI
-
- # Accept sftp as a legacy protocol.
- accepted_schemes = set(PublicCodehostingAPI.supported_schemes)
- accepted_schemes.add('sftp')
- assert scheme in accepted_schemes, "Unknown scheme: %s" % scheme
- host = URI(config.codehosting.supermirror_root).host
- path = '/' + urlutils.escape(unique_name)
- if suffix:
- path = os.path.join(path, suffix)
- return str(URI(scheme=scheme, host=host, path=path))
=== 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-02 22:50:58 +0000
@@ -8,8 +8,11 @@
__metaclass__ = type
__all__ = [
- 'BranchSetAPI', 'IBranchSetAPI', 'IPublicCodehostingAPI',
- 'PublicCodehostingAPI']
+ 'BranchSetAPI',
+ 'IBranchSetAPI',
+ 'IPublicCodehostingAPI',
+ 'PublicCodehostingAPI',
+ ]
from bzrlib import urlutils
@@ -42,6 +45,11 @@
from lp.code.interfaces.branch import IBranch
from lp.code.interfaces.branchlookup import IBranchLookup
from lp.code.interfaces.branchnamespace import get_branch_namespace
+from lp.code.interfaces.codehosting import (
+ BRANCH_ALIAS_PREFIX,
+ compose_public_url,
+ SUPPORTED_SCHEMES,
+ )
from lp.registry.interfaces.distroseries import NoSuchDistroSeries
from lp.registry.interfaces.person import (
IPersonSet,
@@ -171,8 +179,7 @@
def resolve_lp_path(path):
"""Expand the path segment of an lp: URL into a list of branch URLs.
- This method is added to support Bazaar 0.93. It cannot be removed
- until we stop supporting Bazaar 0.93.
+ This method is added to Bazaar in 0.93.
:return: A dict containing a single 'urls' key that maps to a list of
URLs. Clients should use the first URL in the list that they can
@@ -194,12 +201,25 @@
implements(IPublicCodehostingAPI)
- supported_schemes = 'bzr+ssh', 'http'
-
- def _getResultDict(self, branch, suffix=None, supported_schemes=None):
+ def _compose_http_url(unique_name, path, suffix):
+ return compose_public_url('http', unique_name, suffix)
+
+ def _compose_bzr_ssh_url(unique_name, path, suffix):
+ if not path.startswith('~'):
+ path = '%s/%s' % (BRANCH_ALIAS_PREFIX, path)
+ return compose_public_url('bzr+ssh', path, suffix)
+
+ scheme_funcs = {
+ 'bzr+ssh': _compose_bzr_ssh_url,
+ 'http': _compose_http_url,
+ }
+
+ def _getResultDict(self, branch, lp_path, suffix=None,
+ supported_schemes=None):
"""Return a result dict with a list of URLs for the given branch.
:param branch: A Branch object.
+ :param lp_path: The path that was used to traverse to the branch.
:param suffix: The section of the path that follows the branch
specification.
:return: {'urls': [list_of_branch_urls]}.
@@ -210,17 +230,18 @@
return dict(urls=[branch.url])
else:
return self._getUniqueNameResultDict(
- branch.unique_name, suffix, supported_schemes)
+ branch.unique_name, suffix, supported_schemes, lp_path)
def _getUniqueNameResultDict(self, unique_name, suffix=None,
- supported_schemes=None):
- from lp.code.model.branch import compose_public_url
+ supported_schemes=None, path=None):
if supported_schemes is None:
- supported_schemes = self.supported_schemes
+ supported_schemes = SUPPORTED_SCHEMES
+ if path is None:
+ path = unique_name
result = dict(urls=[])
for scheme in supported_schemes:
result['urls'].append(
- compose_public_url(scheme, unique_name, suffix))
+ self.scheme_funcs[scheme](unique_name, path, suffix))
return result
@return_fault
@@ -233,7 +254,7 @@
strip_path = path.strip('/')
if strip_path == '':
raise faults.InvalidBranchIdentifier(path)
- supported_schemes = self.supported_schemes
+ supported_schemes = SUPPORTED_SCHEMES
hot_products = [product.strip() for product
in config.codehosting.hot_products.split(',')]
if strip_path in hot_products:
@@ -270,7 +291,14 @@
raise faults.CannotHaveLinkedBranch(e.component)
except InvalidNamespace, e:
raise faults.InvalidBranchUniqueName(urlutils.escape(e.name))
- return self._getResultDict(branch, suffix, supported_schemes)
+ # Reverse engineer the actual lp_path that is used, so we need to
+ # remove any suffix that may be there from the strip_path.
+ lp_path = strip_path
+ if suffix is not None:
+ # E.g. 'project/trunk/filename.txt' the suffix is 'filename.txt'
+ # we want lp_path to be 'project/trunk'.
+ lp_path = lp_path[:-(len(suffix)+1)]
+ return self._getResultDict(branch, lp_path, suffix, supported_schemes)
def resolve_lp_path(self, path):
"""See `IPublicCodehostingAPI`."""
=== 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-02 22:50:58 +0000
@@ -3,6 +3,8 @@
"""Unit tests for the public codehosting API."""
+from __future__ import with_statement
+
__metaclass__ = type
__all__ = []
@@ -15,17 +17,14 @@
from lazr.uri import URI
from zope.security.proxy import removeSecurityProxy
-from canonical.config import config
-from canonical.launchpad.ftests import (
- login,
- logout,
- )
from canonical.launchpad.xmlrpc import faults
from canonical.testing import DatabaseFunctionalLayer
from lp.code.enums import BranchType
+from lp.code.interfaces.codehosting import BRANCH_ALIAS_PREFIX
+from lp.code.interfaces.linkedbranch import ICanHasLinkedBranch
from lp.code.xmlrpc.branch import PublicCodehostingAPI
from lp.services.xmlrpc import LaunchpadFault
-from lp.testing import TestCaseWithFactory
+from lp.testing import person_logged_in, TestCaseWithFactory
NON_ASCII_NAME = u'nam\N{LATIN SMALL LETTER E WITH ACUTE}'
@@ -36,43 +35,42 @@
layer = DatabaseFunctionalLayer
- def setUp(self):
- """Set up the fixture for these unit tests.
-
- - 'project' is an arbitrary Launchpad project.
- - 'trunk' is a branch on 'project', associated with the development
- focus.
+ def makeProdutWithTrunk(self):
+ """Make a new project with a trunk hosted branch."""
+ product = self.factory.makeProduct()
+ # BranchType is only signficiant insofar as it is not a REMOTE branch.
+ trunk = self.factory.makeProductBranch(
+ branch_type=BranchType.HOSTED, product=product)
+ with person_logged_in(product.owner):
+ ICanHasLinkedBranch(product).setBranch(trunk)
+ return product, trunk
+
+ def assertResolves(self, lp_url_path, public_branch_path, lp_path=None):
+ """Assert that `lp_url_path` path expands to `unique_name`.
+
+ :param public_branch_path: The path that is accessible over http.
+ :param lp_path: The path the short lp alias path for the branch.
"""
- TestCaseWithFactory.setUp(self)
- self.api = PublicCodehostingAPI(None, None)
- self.product = self.factory.makeProduct()
- # Associate 'trunk' with the product's development focus. Use
- # removeSecurityProxy so that we can assign directly to branch.
- trunk_series = removeSecurityProxy(self.product).development_focus
- # BranchType is only signficiant insofar as it is not a REMOTE branch.
- trunk_series.branch = (
- self.factory.makeProductBranch(
- branch_type=BranchType.HOSTED, product=self.product))
-
- def makePrivateBranch(self, **kwargs):
- """Create an arbitrary private branch using `makeBranch`."""
- branch = self.factory.makeAnyBranch(**kwargs)
- naked_branch = removeSecurityProxy(branch)
- naked_branch.private = True
- return branch
-
- def assertResolves(self, lp_url_path, unique_name):
- """Assert that `lp_url_path` path expands to `unique_name`."""
- results = self.api.resolve_lp_path(lp_url_path)
+ api = PublicCodehostingAPI(None, None)
+ results = api.resolve_lp_path(lp_url_path)
+ if lp_path is None:
+ ssh_branch_path = public_branch_path
+ else:
+ ssh_branch_path = '%s/%s' % (BRANCH_ALIAS_PREFIX, lp_path)
# This improves the error message if results happens to be a fault.
if isinstance(results, LaunchpadFault):
raise results
for url in results['urls']:
- self.assertEqual('/' + unique_name, URI(url).path)
+ uri = URI(url)
+ if uri.scheme == 'http':
+ self.assertEqual('/' + public_branch_path, uri.path)
+ else:
+ self.assertEqual('/' + ssh_branch_path, uri.path)
def assertFault(self, lp_url_path, expected_fault):
"""Trying to resolve lp_url_path raises the expected fault."""
- fault = self.api.resolve_lp_path(lp_url_path)
+ api = PublicCodehostingAPI(None, None)
+ fault = api.resolve_lp_path(lp_url_path)
self.assertTrue(
isinstance(fault, xmlrpclib.Fault),
"resolve_lp_path(%r) returned %r, not a Fault."
@@ -87,35 +85,39 @@
# containing a list of these URLs, with the faster and more featureful
# URLs earlier in the list. We use a dict so we can easily add more
# information in the future.
- trunk = self.product.development_focus.branch
- results = self.api.resolve_lp_path(self.product.name)
+ product, trunk = self.makeProdutWithTrunk()
+ api = PublicCodehostingAPI(None, None)
+ results = api.resolve_lp_path(product.name)
urls = [
- 'bzr+ssh://bazaar.launchpad.dev/%s' % trunk.unique_name,
+ 'bzr+ssh://bazaar.launchpad.dev/+branch/%s' % product.name,
'http://bazaar.launchpad.dev/%s' % trunk.unique_name]
self.assertEqual(dict(urls=urls), results)
def test_resultDictForHotProduct(self):
# If 'project-name' is in the config.codehosting.hot_products list,
# lp:project-name will only resolve to the http url.
- config.push(
- 'hot_product',
- '[codehosting]\nhot_products: %s '% self.product.name)
- self.addCleanup(config.pop, 'hot_product')
- trunk = self.product.development_focus.branch
- results = self.api.resolve_lp_path(self.product.name)
+ product, trunk = self.makeProdutWithTrunk()
+ self.pushConfig('codehosting', hot_products=product.name)
+ api = PublicCodehostingAPI(None, None)
+ results = api.resolve_lp_path(product.name)
http_url = 'http://bazaar.launchpad.dev/%s' % trunk.unique_name
self.assertEqual(dict(urls=[http_url]), results)
def test_product_only(self):
# lp:product expands to the branch associated with development focus
- # of the product.
- trunk = self.product.development_focus.branch
- self.assertResolves(self.product.name, trunk.unique_name)
- trunk_series = removeSecurityProxy(self.product).development_focus
- trunk_series.branch = self.factory.makeProductBranch(
- branch_type=BranchType.HOSTED, product=self.product)
- self.assertResolves(
- self.product.name, trunk_series.branch.unique_name)
+ # of the product for the anonymous public access, just to the aliased
+ # short name for bzr+ssh access.
+ product, trunk = self.makeProdutWithTrunk()
+ lp_path = product.name
+ self.assertResolves(lp_path, trunk.unique_name, lp_path)
+
+ def test_product_explicit_dev_series(self):
+ # lp:product/development_focus expands to the branch associated with
+ # development focus of the product for the anonymous public access,
+ # just to the aliased short name for bzr+ssh access.
+ product, trunk = self.makeProdutWithTrunk()
+ lp_path='%s/%s' % (product.name, product.development_focus.name)
+ self.assertResolves(lp_path, trunk.unique_name, lp_path)
def test_product_doesnt_exist(self):
# Return a NoSuchProduct fault if the product doesn't exist.
@@ -165,18 +167,11 @@
def test_product_and_series(self):
# lp:product/series expands to the branch associated with the product
# series 'series' on 'product'.
- series = self.factory.makeSeries(
- product=self.product,
- branch=self.factory.makeProductBranch(product=self.product))
- self.assertResolves(
- '%s/%s' % (self.product.name, series.name),
- series.branch.unique_name)
-
- # We can also use product/series notation to reach trunk.
- self.assertResolves(
- '%s/%s' % (self.product.name,
- self.product.development_focus.name),
- self.product.development_focus.branch.unique_name)
+ product = self.factory.makeProduct()
+ branch = branch=self.factory.makeProductBranch(product=product)
+ series = self.factory.makeSeries(product=product, branch=branch)
+ lp_path = '%s/%s' % (product.name, series.name)
+ self.assertResolves(lp_path, branch.unique_name, lp_path)
def test_development_focus_has_no_branch(self):
# Return a NoLinkedBranch fault if the development focus has no branch
@@ -196,17 +191,19 @@
def test_no_such_product_series(self):
# Return a NoSuchProductSeries fault if there is no series of the
# given name associated with the product.
+ product = self.factory.makeProduct()
self.assertFault(
- '%s/%s' % (self.product.name, "doesntexist"),
- faults.NoSuchProductSeries("doesntexist", self.product))
+ '%s/%s' % (product.name, "doesntexist"),
+ faults.NoSuchProductSeries("doesntexist", product))
def test_no_such_product_series_non_ascii(self):
# lp:product/<non-ascii-string> returns NoSuchProductSeries with the
# name escaped.
+ product = self.factory.makeProduct()
self.assertFault(
- '%s/%s' % (self.product.name, NON_ASCII_NAME),
+ '%s/%s' % (product.name, NON_ASCII_NAME),
faults.NoSuchProductSeries(
- urlutils.escape(NON_ASCII_NAME), self.product))
+ urlutils.escape(NON_ASCII_NAME), product))
def test_no_such_distro_series(self):
# Return a NoSuchDistroSeries fault if there is no series of the given
@@ -254,34 +251,37 @@
def test_branch(self):
# The unique name of a branch resolves to the unique name of the
# branch.
- arbitrary_branch = self.factory.makeAnyBranch()
- self.assertResolves(
- arbitrary_branch.unique_name, arbitrary_branch.unique_name)
- trunk = self.product.development_focus.branch
+ branch = self.factory.makeAnyBranch()
+ self.assertResolves(branch.unique_name, branch.unique_name)
+
+ def test_trunk_accessed_as_branch(self):
+ # A branch that is the development focus for any product can also be
+ # accessed through the branch's unique_name.
+ _ignored, trunk = self.makeProdutWithTrunk()
self.assertResolves(trunk.unique_name, trunk.unique_name)
def test_mirrored_branch(self):
# The unique name of a mirrored branch resolves to the unique name of
# the branch.
- arbitrary_branch = self.factory.makeAnyBranch(
- branch_type=BranchType.MIRRORED)
- self.assertResolves(
- arbitrary_branch.unique_name, arbitrary_branch.unique_name)
+ branch = self.factory.makeAnyBranch(branch_type=BranchType.MIRRORED)
+ self.assertResolves(branch.unique_name, branch.unique_name)
def test_no_such_branch_product(self):
# Resolve paths to branches even if there is no branch of that name.
# We do this so that users can push new branches to lp: URLs.
owner = self.factory.makePerson()
+ product = self.factory.makeProduct()
nonexistent_branch = '~%s/%s/doesntexist' % (
- owner.name, self.product.name)
+ owner.name, product.name)
self.assertResolves(nonexistent_branch, nonexistent_branch)
def test_no_such_branch_product_non_ascii(self):
# A path to a branch that contains non ascii characters will never
# find a branch, but it still resolves rather than erroring.
owner = self.factory.makePerson()
+ product = self.factory.makeProduct()
nonexistent_branch = u'~%s/%s/%s' % (
- owner.name, self.product.name, NON_ASCII_NAME)
+ owner.name, product.name, NON_ASCII_NAME)
self.assertResolves(
nonexistent_branch, urlutils.escape(nonexistent_branch))
@@ -291,8 +291,7 @@
# the '+junk' project, which doesn't actually exist.
owner = self.factory.makePerson()
nonexistent_branch = '~%s/+junk/doesntexist' % owner.name
- self.assertResolves(
- nonexistent_branch, urlutils.escape(nonexistent_branch))
+ self.assertResolves(nonexistent_branch, nonexistent_branch)
def test_no_such_branch_package(self):
# Resolve paths to package branches even if there's no branch of that
@@ -374,26 +373,26 @@
def test_trailing_slashes(self):
# Trailing slashes are trimmed.
# Trailing slashes on lp:product//
- trunk = self.product.development_focus.branch
- self.assertResolves(self.product.name + '/', trunk.unique_name)
- self.assertResolves(self.product.name + '//', trunk.unique_name)
+ product, trunk = self.makeProdutWithTrunk()
+ self.assertResolves(
+ product.name + '/', trunk.unique_name, product.name)
+ self.assertResolves(
+ product.name + '//', trunk.unique_name, product.name)
# Trailing slashes on lp:~owner/product/branch//
- arbitrary_branch = self.factory.makeAnyBranch()
- self.assertResolves(
- arbitrary_branch.unique_name + '/', arbitrary_branch.unique_name)
- self.assertResolves(
- arbitrary_branch.unique_name + '//', arbitrary_branch.unique_name)
+ branch = self.factory.makeAnyBranch()
+ self.assertResolves(branch.unique_name + '/', branch.unique_name)
+ self.assertResolves(branch.unique_name + '//', branch.unique_name)
def test_private_branch(self):
# Invisible branches are resolved as if they didn't exist, so that we
# reveal the least possile amount of information about them.
# For fully specified branch names, this means resolving the lp url.
- arbitrary_branch = self.makePrivateBranch()
+ branch = self.factory.makeAnyBranch(private=True)
# Removing security proxy to get at the unique_name attribute of a
# private branch, and tests are currently running as an anonymous
# user.
- unique_name = removeSecurityProxy(arbitrary_branch).unique_name
+ unique_name = removeSecurityProxy(branch).unique_name
self.assertResolves(unique_name, unique_name)
def test_private_branch_on_series(self):
@@ -404,7 +403,7 @@
# Removing security proxy because we need to be able to get at
# attributes of a private branch and these tests are running as an
# anonymous user.
- branch = removeSecurityProxy(self.makePrivateBranch())
+ branch = self.factory.makeAnyBranch(private=True)
series = self.factory.makeSeries(branch=branch)
self.assertFault(
'%s/%s' % (series.product.name, series.name),
@@ -417,11 +416,9 @@
# development focus. If that branch is private, other views will
# indicate that there is no branch on the development focus. We do the
# same.
- trunk = self.product.development_focus.branch
- naked_trunk = removeSecurityProxy(trunk)
- naked_trunk.private = True
- self.assertFault(
- self.product.name, faults.NoLinkedBranch(self.product))
+ product, trunk = self.makeProdutWithTrunk()
+ removeSecurityProxy(trunk).private = True
+ self.assertFault(product.name, faults.NoLinkedBranch(product))
def test_private_branch_as_user(self):
# We resolve invisible branches as if they don't exist.
@@ -433,19 +430,17 @@
#
# Create the owner explicitly so that we can get its email without
# resorting to removeSecurityProxy.
- email = self.factory.getUniqueEmailAddress()
- arbitrary_branch = self.makePrivateBranch(
- owner=self.factory.makePerson(email=email))
- login(email)
- self.addCleanup(logout)
- self.assertResolves(
- arbitrary_branch.unique_name, arbitrary_branch.unique_name)
+ owner = self.factory.makePerson()
+ branch = self.factory.makeAnyBranch(owner=owner, private=True)
+ path = removeSecurityProxy(branch).unique_name
+ self.assertResolves(path, path)
def test_remote_branch(self):
# For remote branches, return results that link to the actual remote
# branch URL.
branch = self.factory.makeAnyBranch(branch_type=BranchType.REMOTE)
- result = self.api.resolve_lp_path(branch.unique_name)
+ api = PublicCodehostingAPI(None, None)
+ result = api.resolve_lp_path(branch.unique_name)
self.assertEqual([branch.url], result['urls'])
def test_remote_branch_no_url(self):