~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_sftp.py

  • Committer: John Arbash Meinel
  • Date: 2005-11-30 15:35:12 UTC
  • mto: (1185.50.1 jam-integration)
  • mto: This revision was merged to the branch mainline in revision 1518.
  • Revision ID: john@arbash-meinel.com-20051130153512-174444ce2e324335
NEWS

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 Robey Pointer <robey@lag.net>, Canonical Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
import os
 
18
import socket
 
19
import threading
 
20
 
 
21
from bzrlib.tests import TestCaseInTempDir, TestCase
 
22
from bzrlib.tests.test_transport import TestTransportMixIn
 
23
 
 
24
try:
 
25
    import paramiko
 
26
    from stub_sftp import StubServer, StubSFTPServer
 
27
    paramiko_loaded = True
 
28
except ImportError:
 
29
    paramiko_loaded = False
 
30
 
 
31
# XXX: 20051124 jamesh
 
32
# The tests currently pop up a password prompt when an external ssh
 
33
# is used.  This forces the use of the paramiko implementation.
 
34
import bzrlib.transport.sftp
 
35
bzrlib.transport.sftp._ssh_vendor = 'none'
 
36
 
 
37
 
 
38
STUB_SERVER_KEY = """
 
39
-----BEGIN RSA PRIVATE KEY-----
 
40
MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz
 
41
oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/
 
42
d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB
 
43
gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0
 
44
EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon
 
45
soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H
 
46
tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU
 
47
avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA
 
48
4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g
 
49
H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv
 
50
qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV
 
51
HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc
 
52
nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7
 
53
-----END RSA PRIVATE KEY-----
 
54
"""
 
55
    
 
56
 
 
57
class SingleListener (threading.Thread):
 
58
    def __init__(self, callback):
 
59
        threading.Thread.__init__(self)
 
60
        self._callback = callback
 
61
        self._socket = socket.socket()
 
62
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
63
        self._socket.bind(('localhost', 0))
 
64
        self._socket.listen(1)
 
65
        self.port = self._socket.getsockname()[1]
 
66
        self.stop_event = threading.Event()
 
67
 
 
68
    def run(self):
 
69
        s, _ = self._socket.accept()
 
70
        # now close the listen socket
 
71
        self._socket.close()
 
72
        self._callback(s, self.stop_event)
 
73
    
 
74
    def stop(self):
 
75
        self.stop_event.set()
 
76
        # We should consider waiting for the other thread
 
77
        # to stop, because otherwise we get spurious
 
78
        #   bzr: ERROR: Socket exception: Connection reset by peer (54)
 
79
        # because the test suite finishes before the thread has a chance
 
80
        # to close. (Especially when only running a few tests)
 
81
        
 
82
        
 
83
class TestCaseWithSFTPServer (TestCaseInTempDir):
 
84
    """
 
85
    Execute a test case with a stub SFTP server, serving files from the local
 
86
    filesystem over the loopback network.
 
87
    """
 
88
    
 
89
    def _run_server(self, s, stop_event):
 
90
        ssh_server = paramiko.Transport(s)
 
91
        key_file = os.path.join(self._root, 'test_rsa.key')
 
92
        file(key_file, 'w').write(STUB_SERVER_KEY)
 
93
        host_key = paramiko.RSAKey.from_private_key_file(key_file)
 
94
        ssh_server.add_server_key(host_key)
 
95
        server = StubServer(self)
 
96
        ssh_server.set_subsystem_handler('sftp', paramiko.SFTPServer, StubSFTPServer, root=self._root)
 
97
        event = threading.Event()
 
98
        ssh_server.start_server(event, server)
 
99
        event.wait(5.0)
 
100
        stop_event.wait(30.0)
 
101
 
 
102
    def setUp(self):
 
103
        TestCaseInTempDir.setUp(self)
 
104
        self._root = self.test_dir
 
105
        self._is_setup = False
 
106
 
 
107
    def delayed_setup(self):
 
108
        # some tests are just stubs that call setUp and then immediately call
 
109
        # tearDwon.  so don't create the port listener until get_transport is
 
110
        # called and we know we're in an actual test.
 
111
        if self._is_setup:
 
112
            return
 
113
        self._listener = SingleListener(self._run_server)
 
114
        self._listener.setDaemon(True)
 
115
        self._listener.start()        
 
116
        self._sftp_url = 'sftp://foo:bar@localhost:%d/' % (self._listener.port,)
 
117
        self._is_setup = True
 
118
        
 
119
    def tearDown(self):
 
120
        try:
 
121
            self._listener.stop()
 
122
        except AttributeError:
 
123
            pass
 
124
        TestCaseInTempDir.tearDown(self)
 
125
 
 
126
        
 
127
class SFTPTransportTest (TestCaseWithSFTPServer, TestTransportMixIn):
 
128
    readonly = False
 
129
 
 
130
    def setUp(self):
 
131
        TestCaseWithSFTPServer.setUp(self)
 
132
        self.sftplogs = []
 
133
 
 
134
    def log(self, *args):
 
135
        """Override the default log to grab sftp server messages"""
 
136
        TestCaseWithSFTPServer.log(self, *args)
 
137
        if args and args[0].startswith('sftpserver'):
 
138
            self.sftplogs.append(args[0])
 
139
 
 
140
    def get_transport(self):
 
141
        self.delayed_setup()
 
142
        from bzrlib.transport.sftp import SFTPTransport
 
143
        url = self._sftp_url
 
144
        return SFTPTransport(url)
 
145
 
 
146
    def test_sftp_locks(self):
 
147
        from bzrlib.errors import LockError
 
148
        t = self.get_transport()
 
149
 
 
150
        l = t.lock_write('bogus')
 
151
        self.failUnlessExists('bogus.write-lock')
 
152
 
 
153
        # Don't wait for the lock, locking an already locked
 
154
        # file should raise an assert
 
155
        self.assertRaises(LockError, t.lock_write, 'bogus')
 
156
 
 
157
        l.unlock()
 
158
        self.failIf(os.path.lexists('bogus.write-lock'))
 
159
 
 
160
        open('something.write-lock', 'wb').write('fake lock\n')
 
161
        self.assertRaises(LockError, t.lock_write, 'something')
 
162
        os.remove('something.write-lock')
 
163
 
 
164
        l = t.lock_write('something')
 
165
 
 
166
        l2 = t.lock_write('bogus')
 
167
 
 
168
        l.unlock()
 
169
        l2.unlock()
 
170
 
 
171
    def test_multiple_connections(self):
 
172
        t = self.get_transport()
 
173
        self.assertEquals(self.sftplogs, 
 
174
                ['sftpserver - authorizing: foo'
 
175
               , 'sftpserver - channel request: session, 1'])
 
176
        self.sftplogs = []
 
177
        # The second request should reuse the first connection
 
178
        # SingleListener only allows for a single connection,
 
179
        # So the next line fails unless the connection is reused
 
180
        t2 = self.get_transport()
 
181
        self.assertEquals(self.sftplogs, [])
 
182
 
 
183
 
 
184
class FakeSFTPTransport (object):
 
185
    _sftp = object()
 
186
fake = FakeSFTPTransport()
 
187
 
 
188
 
 
189
class SFTPNonServerTest(TestCase):
 
190
    def test_parse_url(self):
 
191
        from bzrlib.transport.sftp import SFTPTransport
 
192
        s = SFTPTransport('sftp://simple.example.com/%2fhome/source', clone_from=fake)
 
193
        self.assertEquals(s._host, 'simple.example.com')
 
194
        self.assertEquals(s._port, 22)
 
195
        self.assertEquals(s._path, '/home/source')
 
196
        self.failUnless(s._password is None)
 
197
 
 
198
        self.assertEquals(s.base, 'sftp://simple.example.com/%2Fhome/source')
 
199
        
 
200
        s = SFTPTransport('sftp://ro%62ey:h%40t@example.com:2222/relative', clone_from=fake)
 
201
        self.assertEquals(s._host, 'example.com')
 
202
        self.assertEquals(s._port, 2222)
 
203
        self.assertEquals(s._username, 'robey')
 
204
        self.assertEquals(s._password, 'h@t')
 
205
        self.assertEquals(s._path, 'relative')
 
206
 
 
207
        # Base should not keep track of the password
 
208
        self.assertEquals(s.base, 'sftp://robey@example.com:2222/relative')
 
209
 
 
210
        # Double slash should be accepted instead of using %2F
 
211
        s = SFTPTransport('sftp://user@example.com:22//absolute/path', clone_from=fake)
 
212
        self.assertEquals(s._host, 'example.com')
 
213
        self.assertEquals(s._port, 22)
 
214
        self.assertEquals(s._username, 'user')
 
215
        self.assertEquals(s._password, None)
 
216
        self.assertEquals(s._path, '/absolute/path')
 
217
 
 
218
        # Also, don't show the port if it is the default 22
 
219
        self.assertEquals(s.base, 'sftp://user@example.com/%2Fabsolute/path')
 
220
 
 
221
    def test_parse_invalid_url(self):
 
222
        from bzrlib.transport.sftp import SFTPTransport, SFTPTransportError
 
223
        try:
 
224
            s = SFTPTransport('sftp://lilypond.org:~janneke/public_html/bzr/gub',
 
225
                              clone_from=fake)
 
226
            self.fail('expected exception not raised')
 
227
        except SFTPTransportError, e:
 
228
            self.assertEquals(str(e), 
 
229
                    '~janneke: invalid port number')
 
230
 
 
231
 
 
232
class SFTPBranchTest(TestCaseWithSFTPServer):
 
233
    """Test some stuff when accessing a bzr Branch over sftp"""
 
234
 
 
235
    def test_lock_file(self):
 
236
        """Make sure that a Branch accessed over sftp tries to lock itself."""
 
237
        from bzrlib.branch import Branch
 
238
 
 
239
        self.delayed_setup()
 
240
        b = Branch.initialize(self._sftp_url)
 
241
        self.failUnlessExists('.bzr/')
 
242
        self.failUnlessExists('.bzr/branch-format')
 
243
        self.failUnlessExists('.bzr/branch-lock')
 
244
 
 
245
        self.failIf(os.path.lexists('.bzr/branch-lock.write-lock'))
 
246
        b.lock_write()
 
247
        self.failUnlessExists('.bzr/branch-lock.write-lock')
 
248
        b.unlock()
 
249
        self.failIf(os.path.lexists('.bzr/branch-lock.write-lock'))
 
250
 
 
251
 
 
252
if not paramiko_loaded:
 
253
    # TODO: Skip these
 
254
    del SFTPTransportTest
 
255
    del SFTPNonServerTest
 
256
    del SFTPBranchTest