~launchpad-pqm/launchpad/devel

13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
1
# Copyright 2009-2011 Canonical Ltd.  This software is licensed under the
2
# GNU Affero General Public License version 3 (see the file LICENSE).
3
4
"""Tests for the safe branch open code."""
5
6
7
__metaclass__ = type
8
13811.1.1 by Jeroen Vermeulen
More lint.
9
from bzrlib.branch import (
10
    Branch,
11
    BranchReferenceFormat,
12
    BzrBranchFormat7,
13
    )
14
from bzrlib.bzrdir import (
15
    BzrDir,
16
    BzrDirMetaFormat1,
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
17
    BzrProber,
13811.1.1 by Jeroen Vermeulen
More lint.
18
    )
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
19
from bzrlib.controldir import ControlDirFormat
20
from bzrlib.errors import NotBranchError
7675.1225.1 by Jelmer Vernooij
Merge stable.
21
from bzrlib.repofmt.knitpack_repo import RepositoryFormatKnitPack1
13811.1.1 by Jeroen Vermeulen
More lint.
22
from bzrlib.tests import TestCaseWithTransport
23
from bzrlib.transport import chroot
13637.3.5 by Jelmer Vernooij
Readd tests.
24
from lazr.uri import URI
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
25
13637.3.5 by Jelmer Vernooij
Readd tests.
26
from lp.codehosting.safe_open import (
27
    BadUrl,
28
    BlacklistPolicy,
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
29
    BranchLoopError,
30
    BranchReferenceForbidden,
13811.1.1 by Jeroen Vermeulen
More lint.
31
    safe_open,
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
32
    SafeBranchOpener,
13604.1.7 by Jelmer Vernooij
Fix some imports.
33
    WhitelistPolicy,
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
34
    )
35
from lp.testing import TestCase
36
37
38
class TestSafeBranchOpenerCheckAndFollowBranchReference(TestCase):
39
    """Unit tests for `SafeBranchOpener.checkAndFollowBranchReference`."""
40
13677.5.5 by Jelmer Vernooij
more docstrings, tests.
41
    def setUp(self):
42
        super(TestSafeBranchOpenerCheckAndFollowBranchReference, self).setUp()
43
        SafeBranchOpener.install_hook()
44
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
45
    class StubbedSafeBranchOpener(SafeBranchOpener):
46
        """SafeBranchOpener that provides canned answers.
47
48
        We implement the methods we need to to be able to control all the
49
        inputs to the `BranchMirrorer.checkSource` method, which is what is
50
        being tested in this class.
51
        """
52
53
        def __init__(self, references, policy):
54
            parent_cls = TestSafeBranchOpenerCheckAndFollowBranchReference
55
            super(parent_cls.StubbedSafeBranchOpener, self).__init__(policy)
56
            self._reference_values = {}
57
            for i in range(len(references) - 1):
13811.1.1 by Jeroen Vermeulen
More lint.
58
                self._reference_values[references[i]] = references[i + 1]
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
59
            self.follow_reference_calls = []
60
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
61
        def followReference(self, url):
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
62
            self.follow_reference_calls.append(url)
63
            return self._reference_values[url]
64
65
    def makeBranchOpener(self, should_follow_references, references,
66
                         unsafe_urls=None):
67
        policy = BlacklistPolicy(should_follow_references, unsafe_urls)
68
        opener = self.StubbedSafeBranchOpener(references, policy)
69
        return opener
70
71
    def testCheckInitialURL(self):
72
        # checkSource rejects all URLs that are not allowed.
73
        opener = self.makeBranchOpener(None, [], set(['a']))
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
74
        self.assertRaises(
75
            BadUrl, opener.checkAndFollowBranchReference, 'a')
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
76
77
    def testNotReference(self):
78
        # When branch references are forbidden, checkAndFollowBranchReference
79
        # does not raise on non-references.
80
        opener = self.makeBranchOpener(False, ['a', None])
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
81
        self.assertEquals(
82
            'a', opener.checkAndFollowBranchReference('a'))
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
83
        self.assertEquals(['a'], opener.follow_reference_calls)
84
85
    def testBranchReferenceForbidden(self):
86
        # checkAndFollowBranchReference raises BranchReferenceForbidden if
87
        # branch references are forbidden and the source URL points to a
88
        # branch reference.
89
        opener = self.makeBranchOpener(False, ['a', 'b'])
90
        self.assertRaises(
91
            BranchReferenceForbidden,
14482.1.6 by Jelmer Vernooij
Fix tests.
92
            opener.checkAndFollowBranchReference, 'a')
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
93
        self.assertEquals(['a'], opener.follow_reference_calls)
94
95
    def testAllowedReference(self):
96
        # checkAndFollowBranchReference does not raise if following references
97
        # is allowed and the source URL points to a branch reference to a
98
        # permitted location.
99
        opener = self.makeBranchOpener(True, ['a', 'b', None])
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
100
        self.assertEquals(
101
            'b', opener.checkAndFollowBranchReference('a'))
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
102
        self.assertEquals(['a', 'b'], opener.follow_reference_calls)
103
104
    def testCheckReferencedURLs(self):
105
        # checkAndFollowBranchReference checks if the URL a reference points
106
        # to is safe.
107
        opener = self.makeBranchOpener(
108
            True, ['a', 'b', None], unsafe_urls=set('b'))
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
109
        self.assertRaises(
110
            BadUrl, opener.checkAndFollowBranchReference, 'a')
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
111
        self.assertEquals(['a'], opener.follow_reference_calls)
112
113
    def testSelfReferencingBranch(self):
114
        # checkAndFollowBranchReference raises BranchReferenceLoopError if
115
        # following references is allowed and the source url points to a
116
        # self-referencing branch reference.
117
        opener = self.makeBranchOpener(True, ['a', 'a'])
118
        self.assertRaises(
14482.1.8 by Jelmer Vernooij
Add more tests in test_safe_open.
119
            BranchLoopError, opener.checkAndFollowBranchReference, 'a')
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
120
        self.assertEquals(['a'], opener.follow_reference_calls)
121
122
    def testBranchReferenceLoop(self):
123
        # checkAndFollowBranchReference raises BranchReferenceLoopError if
124
        # following references is allowed and the source url points to a loop
125
        # of branch references.
126
        references = ['a', 'b', 'a']
127
        opener = self.makeBranchOpener(True, references)
128
        self.assertRaises(
14482.1.8 by Jelmer Vernooij
Add more tests in test_safe_open.
129
            BranchLoopError, opener.checkAndFollowBranchReference, 'a')
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
130
        self.assertEquals(['a', 'b'], opener.follow_reference_calls)
131
132
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
133
class TrackingProber(BzrProber):
134
    """Subclass of BzrProber which tracks URLs it has been asked to open."""
135
136
    seen_urls = []
137
138
    @classmethod
139
    def probe_transport(klass, transport):
140
        klass.seen_urls.append(transport.base)
141
        return BzrProber.probe_transport(transport)
142
143
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
144
class TestSafeBranchOpenerStacking(TestCaseWithTransport):
145
13677.5.4 by Jelmer Vernooij
Install hook globally.
146
    def setUp(self):
147
        super(TestSafeBranchOpenerStacking, self).setUp()
148
        SafeBranchOpener.install_hook()
149
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
150
    def makeBranchOpener(self, allowed_urls, probers=None):
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
151
        policy = WhitelistPolicy(True, allowed_urls, True)
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
152
        return SafeBranchOpener(policy, probers)
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
153
154
    def makeBranch(self, path, branch_format, repository_format):
155
        """Make a Bazaar branch at 'path' with the given formats."""
156
        bzrdir_format = BzrDirMetaFormat1()
157
        bzrdir_format.set_branch_format(branch_format)
158
        bzrdir = self.make_bzrdir(path, format=bzrdir_format)
159
        repository_format.initialize(bzrdir)
160
        return bzrdir.create_branch()
161
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
162
    def testProbers(self):
163
        # Only the specified probers should be used
164
        b = self.make_branch('branch')
165
        opener = self.makeBranchOpener([b.base], probers=[])
166
        self.assertRaises(NotBranchError, opener.open, b.base)
167
        opener = self.makeBranchOpener([b.base], probers=[BzrProber])
168
        self.assertEquals(b.base, opener.open(b.base).base)
169
170
    def testDefaultProbers(self):
171
        # If no probers are specified to the constructor
172
        # of SafeBranchOpener, then a safe set will be used,
173
        # rather than all probers registered in bzr.
174
        self.addCleanup(ControlDirFormat.unregister_prober, TrackingProber)
175
        ControlDirFormat.register_prober(TrackingProber)
176
        # Open a location without any branches, so that all probers are
177
        # tried.
178
        # First, check that the TrackingProber tracks correctly.
179
        TrackingProber.seen_urls = []
180
        opener = self.makeBranchOpener(["."], probers=[TrackingProber])
181
        self.assertRaises(NotBranchError, opener.open, ".")
182
        self.assertEquals(1, len(TrackingProber.seen_urls))
183
        TrackingProber.seen_urls = []
184
        # And make sure it's registered in such a way that BzrDir.open would
185
        # use it.
186
        self.assertRaises(NotBranchError, BzrDir.open, ".")
187
        self.assertEquals(1, len(TrackingProber.seen_urls))
188
        TrackingProber.seen_urls = []
189
        # Make sure that SafeBranchOpener doesn't use it if no
190
        # probers were specified
191
        opener = self.makeBranchOpener(["."])
192
        self.assertRaises(NotBranchError, opener.open, ".")
193
        self.assertEquals(0, len(TrackingProber.seen_urls))
194
13604.1.6 by Jelmer Vernooij
Move SafeBranchOpener to its own file.
195
    def testAllowedURL(self):
196
        # checkSource does not raise an exception for branches stacked on
197
        # branches with allowed URLs.
198
        stacked_on_branch = self.make_branch('base-branch', format='1.6')
199
        stacked_branch = self.make_branch('stacked-branch', format='1.6')
200
        stacked_branch.set_stacked_on_url(stacked_on_branch.base)
201
        opener = self.makeBranchOpener(
202
            [stacked_branch.base, stacked_on_branch.base])
203
        # This doesn't raise an exception.
204
        opener.open(stacked_branch.base)
205
206
    def testUnstackableRepository(self):
207
        # checkSource treats branches with UnstackableRepositoryFormats as
208
        # being not stacked.
209
        branch = self.makeBranch(
210
            'unstacked', BzrBranchFormat7(), RepositoryFormatKnitPack1())
211
        opener = self.makeBranchOpener([branch.base])
212
        # This doesn't raise an exception.
213
        opener.open(branch.base)
214
215
    def testAllowedRelativeURL(self):
216
        # checkSource passes on absolute urls to checkOneURL, even if the
217
        # value of stacked_on_location in the config is set to a relative URL.
218
        stacked_on_branch = self.make_branch('base-branch', format='1.6')
219
        stacked_branch = self.make_branch('stacked-branch', format='1.6')
220
        stacked_branch.set_stacked_on_url('../base-branch')
221
        opener = self.makeBranchOpener(
222
            [stacked_branch.base, stacked_on_branch.base])
223
        # Note that stacked_on_branch.base is not '../base-branch', it's an
224
        # absolute URL.
225
        self.assertNotEqual('../base-branch', stacked_on_branch.base)
226
        # This doesn't raise an exception.
227
        opener.open(stacked_branch.base)
228
229
    def testAllowedRelativeNested(self):
230
        # Relative URLs are resolved relative to the stacked branch.
231
        self.get_transport().mkdir('subdir')
232
        a = self.make_branch('subdir/a', format='1.6')
233
        b = self.make_branch('b', format='1.6')
234
        b.set_stacked_on_url('../subdir/a')
235
        c = self.make_branch('subdir/c', format='1.6')
236
        c.set_stacked_on_url('../../b')
237
        opener = self.makeBranchOpener([c.base, b.base, a.base])
238
        # This doesn't raise an exception.
239
        opener.open(c.base)
240
241
    def testForbiddenURL(self):
242
        # checkSource raises a BadUrl exception if a branch is stacked on a
243
        # branch with a forbidden URL.
244
        stacked_on_branch = self.make_branch('base-branch', format='1.6')
245
        stacked_branch = self.make_branch('stacked-branch', format='1.6')
246
        stacked_branch.set_stacked_on_url(stacked_on_branch.base)
247
        opener = self.makeBranchOpener([stacked_branch.base])
248
        self.assertRaises(BadUrl, opener.open, stacked_branch.base)
249
250
    def testForbiddenURLNested(self):
251
        # checkSource raises a BadUrl exception if a branch is stacked on a
252
        # branch that is in turn stacked on a branch with a forbidden URL.
253
        a = self.make_branch('a', format='1.6')
254
        b = self.make_branch('b', format='1.6')
255
        b.set_stacked_on_url(a.base)
256
        c = self.make_branch('c', format='1.6')
257
        c.set_stacked_on_url(b.base)
258
        opener = self.makeBranchOpener([c.base, b.base])
259
        self.assertRaises(BadUrl, opener.open, c.base)
260
261
    def testSelfStackedBranch(self):
262
        # checkSource raises StackingLoopError if a branch is stacked on
263
        # itself. This avoids infinite recursion errors.
264
        a = self.make_branch('a', format='1.6')
265
        # Bazaar 1.17 and up make it harder to create branches like this.
266
        # It's still worth testing that we don't blow up in the face of them,
267
        # so we grovel around a bit to create one anyway.
268
        a.get_config().set_user_option('stacked_on_location', a.base)
269
        opener = self.makeBranchOpener([a.base])
270
        self.assertRaises(BranchLoopError, opener.open, a.base)
271
272
    def testLoopStackedBranch(self):
273
        # checkSource raises StackingLoopError if a branch is stacked in such
274
        # a way so that it is ultimately stacked on itself. e.g. a stacked on
275
        # b stacked on a.
276
        a = self.make_branch('a', format='1.6')
277
        b = self.make_branch('b', format='1.6')
278
        a.set_stacked_on_url(b.base)
279
        b.set_stacked_on_url(a.base)
280
        opener = self.makeBranchOpener([a.base, b.base])
281
        self.assertRaises(BranchLoopError, opener.open, a.base)
282
        self.assertRaises(BranchLoopError, opener.open, b.base)
13637.3.5 by Jelmer Vernooij
Readd tests.
283
13756.3.14 by Jelmer Vernooij
More tests.
284
    def testCustomOpener(self):
285
        # A custom function for opening a control dir can be specified.
286
        a = self.make_branch('a', format='2a')
287
        b = self.make_branch('b', format='2a')
288
        b.set_stacked_on_url(a.base)
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
289
290
        TrackingProber.seen_urls = []
291
        opener = self.makeBranchOpener(
292
            [a.base, b.base], probers=[TrackingProber])
293
        opener.open(b.base)
294
        self.assertEquals(
295
            set(TrackingProber.seen_urls), set([b.base, a.base]))
13756.3.14 by Jelmer Vernooij
More tests.
296
297
    def testCustomOpenerWithBranchReference(self):
298
        # A custom function for opening a control dir can be specified.
299
        a = self.make_branch('a', format='2a')
300
        b_dir = self.make_bzrdir('b')
301
        b = BranchReferenceFormat().initialize(b_dir, target_branch=a)
14517.1.3 by Jelmer Vernooij
Re-merge always loading of foreign plugins.
302
        TrackingProber.seen_urls = []
303
        opener = self.makeBranchOpener(
304
            [a.base, b.base], probers=[TrackingProber])
305
        opener.open(b.base)
306
        self.assertEquals(
307
            set(TrackingProber.seen_urls), set([b.base, a.base]))
13756.3.14 by Jelmer Vernooij
More tests.
308
13637.3.5 by Jelmer Vernooij
Readd tests.
309
310
class TestSafeOpen(TestCaseWithTransport):
311
    """Tests for `safe_open`."""
312
13677.5.4 by Jelmer Vernooij
Install hook globally.
313
    def setUp(self):
314
        super(TestSafeOpen, self).setUp()
315
        SafeBranchOpener.install_hook()
316
13677.5.5 by Jelmer Vernooij
more docstrings, tests.
317
    def test_hook_does_not_interfere(self):
318
        # The transform_fallback_location hook does not interfere with regular
319
        # stacked branch access outside of safe_open.
320
        self.make_branch('stacked')
321
        self.make_branch('stacked-on')
322
        Branch.open('stacked').set_stacked_on_url('../stacked-on')
323
        Branch.open('stacked')
324
13637.3.5 by Jelmer Vernooij
Readd tests.
325
    def get_chrooted_scheme(self, relpath):
326
        """Create a server that is chrooted to `relpath`.
327
328
        :return: ``(scheme, get_url)`` where ``scheme`` is the scheme of the
329
            chroot server and ``get_url`` returns URLs on said server.
330
        """
331
        transport = self.get_transport(relpath)
332
        chroot_server = chroot.ChrootServer(transport)
333
        chroot_server.start_server()
334
        self.addCleanup(chroot_server.stop_server)
13811.1.1 by Jeroen Vermeulen
More lint.
335
13637.3.5 by Jelmer Vernooij
Readd tests.
336
        def get_url(relpath):
337
            return chroot_server.get_url() + relpath
13811.1.1 by Jeroen Vermeulen
More lint.
338
13637.3.5 by Jelmer Vernooij
Readd tests.
339
        return URI(chroot_server.get_url()).scheme, get_url
340
341
    def test_stacked_within_scheme(self):
342
        # A branch that is stacked on a URL of the same scheme is safe to
343
        # open.
344
        self.get_transport().mkdir('inside')
345
        self.make_branch('inside/stacked')
346
        self.make_branch('inside/stacked-on')
347
        scheme, get_chrooted_url = self.get_chrooted_scheme('inside')
348
        Branch.open(get_chrooted_url('stacked')).set_stacked_on_url(
349
            get_chrooted_url('stacked-on'))
350
        safe_open(scheme, get_chrooted_url('stacked'))
351
352
    def test_stacked_outside_scheme(self):
353
        # A branch that is stacked on a URL that is not of the same scheme is
354
        # not safe to open.
355
        self.get_transport().mkdir('inside')
356
        self.get_transport().mkdir('outside')
357
        self.make_branch('inside/stacked')
358
        self.make_branch('outside/stacked-on')
359
        scheme, get_chrooted_url = self.get_chrooted_scheme('inside')
360
        Branch.open(get_chrooted_url('stacked')).set_stacked_on_url(
361
            self.get_url('outside/stacked-on'))
362
        self.assertRaises(
363
            BadUrl, safe_open, scheme, get_chrooted_url('stacked'))