Ticket #531: test_sftp.py

File test_sftp.py, 43.6 KB (added by davidsarah, at 2010-05-12T06:13:30Z)

Tests for new SFTP implementation

Line 
1
2import re
3from stat import S_IFREG, S_IFDIR
4
5from twisted.trial import unittest
6from twisted.internet import defer
7from twisted.python.failure import Failure
8
9sftp = None
10sftpd = None
11have_pycrypto = False
12try:
13    from Crypto import Util
14    Util  # hush pyflakes
15    have_pycrypto = True
16except ImportError:
17    pass
18
19if have_pycrypto:
20    from twisted.conch.ssh import filetransfer as sftp
21    from allmydata.frontends import sftpd
22
23# FIXME remove this
24#from twisted.internet.base import DelayedCall
25#DelayedCall.debug = True
26
27import sys, traceback
28
29"""
30def trace_exceptions(frame, event, arg):
31    if event != 'exception':
32        return
33    co = frame.f_code
34    func_name = co.co_name
35    line_no = frame.f_lineno
36    filename = co.co_filename
37    exc_type, exc_value, exc_traceback = arg
38    print 'Tracing exception: %r %r on line %r of %r in %r' % \
39        (exc_type.__name__, exc_value, line_no, func_name, filename)
40
41def trace_calls(frame, event, arg):
42    if event != 'call':
43        return
44    return trace_exceptions
45
46sys.settrace(trace_calls)
47"""
48
49timeout = 30
50
51from allmydata.interfaces import IDirectoryNode, ExistingChildError, NoSuchChildError
52from allmydata.mutable.common import NotWriteableError
53
54from allmydata.util.consumer import download_to_data
55from allmydata.immutable import upload
56from allmydata.test.no_network import GridTestMixin
57from allmydata.test.common import ShouldFailMixin
58
59class Handler(GridTestMixin, ShouldFailMixin, unittest.TestCase):
60    """This is a no-network unit test of the SFTPHandler class."""
61
62    if not have_pycrypto:
63        skip = "SFTP support requires pycrypto, which is not installed"
64
65    def shouldFailWithSFTPError(self, expected_code, which, callable, *args, **kwargs):
66        assert isinstance(expected_code, int), repr(expected_code)
67        assert isinstance(which, str), repr(which)
68        s = traceback.format_stack()
69        d = defer.maybeDeferred(callable, *args, **kwargs)
70        def _done(res):
71            if isinstance(res, Failure):
72                res.trap(sftp.SFTPError)
73                self.failUnlessEqual(res.value.code, expected_code,
74                                     "%s was supposed to raise SFTPError(%d), not SFTPError(%d): %s" %
75                                     (which, expected_code, res.value.code, res))
76            else:
77                print '@' + '@'.join(s)
78                self.fail("%s was supposed to raise SFTPError(%d), not get '%s'" %
79                          (which, expected_code, res))
80        d.addBoth(_done)
81        return d
82
83    def _set_up(self, basedir, num_clients=1, num_servers=10):
84        self.basedir = "sftp/" + basedir
85        self.set_up_grid(num_clients=num_clients, num_servers=num_servers)
86
87        def check_abort():
88            pass
89        self.client = self.g.clients[0]
90        self.username = "alice"
91        self.convergence = "convergence"
92
93        d = self.client.create_dirnode()
94        def _created_root(node):
95            self.root = node
96            self.root_uri = node.get_uri()
97            self.user = sftpd.SFTPUser(check_abort, self.client, self.root, self.username, self.convergence)
98            self.handler = sftpd.SFTPHandler(self.user)
99        d.addCallback(_created_root)
100        return d
101
102    def _set_up_tree(self):
103        d = self.client.create_mutable_file("mutable file contents")
104        d.addCallback(lambda node: self.root.set_node(u"mutable", node))
105        def _created_mutable(n):
106            self.mutable = n
107            self.mutable_uri = n.get_uri()
108        d.addCallback(_created_mutable)
109
110        d.addCallback(lambda ign:
111                      self.root._create_and_validate_node(None, self.mutable.get_readonly_uri(), name=u"readonly"))
112        d.addCallback(lambda node: self.root.set_node(u"readonly", node))
113        def _created_readonly(n):
114            self.readonly = n
115            self.readonly_uri = n.get_uri()
116        d.addCallback(_created_readonly)
117
118        gross = upload.Data("0123456789" * 101, None)
119        d.addCallback(lambda ign: self.root.add_file(u"gro\u00DF", gross))
120        def _created_gross(n):
121            self.gross = n
122            self.gross_uri = n.get_uri()
123        d.addCallback(_created_gross)
124
125        small = upload.Data("0123456789", None)
126        d.addCallback(lambda ign: self.root.add_file(u"small", small))
127        def _created_small(n):
128            self.small = n
129            self.small_uri = n.get_uri()
130        d.addCallback(_created_small)
131
132        small2 = upload.Data("Small enough for a LIT too", None)
133        d.addCallback(lambda ign: self.root.add_file(u"small2", small2))
134        def _created_small2(n):
135            self.small2 = n
136            self.small2_uri = n.get_uri()
137        d.addCallback(_created_small2)
138
139        empty_litdir_uri = "URI:DIR2-LIT:"
140
141        # contains one child which is itself also LIT:
142        tiny_litdir_uri = "URI:DIR2-LIT:gqytunj2onug64tufqzdcosvkjetutcjkq5gw4tvm5vwszdgnz5hgyzufqydulbshj5x2lbm"
143
144        unknown_uri = "x-tahoe-crazy://I_am_from_the_future."
145
146        d.addCallback(lambda ign: self.root._create_and_validate_node(None, empty_litdir_uri, name=u"empty_lit_dir"))
147        def _created_empty_lit_dir(n):
148            self.empty_lit_dir = n
149            self.empty_lit_dir_uri = n.get_uri()
150            self.root.set_node(u"empty_lit_dir", n)
151        d.addCallback(_created_empty_lit_dir)
152
153        d.addCallback(lambda ign: self.root._create_and_validate_node(None, tiny_litdir_uri, name=u"tiny_lit_dir"))
154        def _created_tiny_lit_dir(n):
155            self.tiny_lit_dir = n
156            self.tiny_lit_dir_uri = n.get_uri()
157            self.root.set_node(u"tiny_lit_dir", n)
158        d.addCallback(_created_tiny_lit_dir)
159
160        d.addCallback(lambda ign: self.root._create_and_validate_node(None, unknown_uri, name=u"unknown"))
161        def _created_unknown(n):
162            self.unknown = n
163            self.unknown_uri = n.get_uri()
164            self.root.set_node(u"unknown", n)
165        d.addCallback(_created_unknown)
166
167        d.addCallback(lambda ign: self.root.set_node(u"loop", self.root))
168        return d
169
170    def test_basic(self):
171        d = self._set_up("basic")
172        def _check(ign):
173            # Test operations that have no side-effects, and don't need the tree.
174
175            version = self.handler.gotVersion(3, {})
176            self.failUnless(isinstance(version, dict))
177
178            self.failUnlessEqual(self.handler._path_from_string(""), [])
179            self.failUnlessEqual(self.handler._path_from_string("/"), [])
180            self.failUnlessEqual(self.handler._path_from_string("."), [])
181            self.failUnlessEqual(self.handler._path_from_string("//"), [])
182            self.failUnlessEqual(self.handler._path_from_string("/."), [])
183            self.failUnlessEqual(self.handler._path_from_string("/./"), [])
184            self.failUnlessEqual(self.handler._path_from_string("foo"), [u"foo"])
185            self.failUnlessEqual(self.handler._path_from_string("/foo"), [u"foo"])
186            self.failUnlessEqual(self.handler._path_from_string("foo/"), [u"foo"])
187            self.failUnlessEqual(self.handler._path_from_string("/foo/"), [u"foo"])
188            self.failUnlessEqual(self.handler._path_from_string("foo/bar"), [u"foo", u"bar"])
189            self.failUnlessEqual(self.handler._path_from_string("/foo/bar"), [u"foo", u"bar"])
190            self.failUnlessEqual(self.handler._path_from_string("foo/bar//"), [u"foo", u"bar"])
191            self.failUnlessEqual(self.handler._path_from_string("/foo/bar//"), [u"foo", u"bar"])
192            self.failUnlessEqual(self.handler._path_from_string("foo/../bar"), [u"bar"])
193            self.failUnlessEqual(self.handler._path_from_string("/foo/../bar"), [u"bar"])
194            self.failUnlessEqual(self.handler._path_from_string("../bar"), [u"bar"])
195            self.failUnlessEqual(self.handler._path_from_string("/../bar"), [u"bar"])
196
197            self.failUnlessEqual(self.handler.realPath(""), "/")
198            self.failUnlessEqual(self.handler.realPath("/"), "/")
199            self.failUnlessEqual(self.handler.realPath("."), "/")
200            self.failUnlessEqual(self.handler.realPath("//"), "/")
201            self.failUnlessEqual(self.handler.realPath("/."), "/")
202            self.failUnlessEqual(self.handler.realPath("/./"), "/")
203            self.failUnlessEqual(self.handler.realPath("foo"), "/foo")
204            self.failUnlessEqual(self.handler.realPath("/foo"), "/foo")
205            self.failUnlessEqual(self.handler.realPath("foo/"), "/foo")
206            self.failUnlessEqual(self.handler.realPath("/foo/"), "/foo")
207            self.failUnlessEqual(self.handler.realPath("foo/bar"), "/foo/bar")
208            self.failUnlessEqual(self.handler.realPath("/foo/bar"), "/foo/bar")
209            self.failUnlessEqual(self.handler.realPath("foo/bar//"), "/foo/bar")
210            self.failUnlessEqual(self.handler.realPath("/foo/bar//"), "/foo/bar")
211            self.failUnlessEqual(self.handler.realPath("foo/../bar"), "/bar")
212            self.failUnlessEqual(self.handler.realPath("/foo/../bar"), "/bar")
213            self.failUnlessEqual(self.handler.realPath("../bar"), "/bar")
214            self.failUnlessEqual(self.handler.realPath("/../bar"), "/bar")
215        d.addCallback(_check)
216
217        d.addCallback(lambda ign:
218            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "_path_from_string invalid UTF-8",
219                                         self.handler._path_from_string, "\xFF"))
220        d.addCallback(lambda ign:
221            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "realPath invalid UTF-8",
222                                         self.handler.realPath, "\xFF"))
223
224        return d
225
226    def test_raise_error(self):
227        self.failUnlessEqual(sftpd._raise_error(None), None)
228       
229        d = defer.succeed(None)
230        d.addCallback(lambda ign:
231            self.shouldFailWithSFTPError(sftp.FX_FAILURE, "_raise_error SFTPError",
232                                         sftpd._raise_error, Failure(sftp.SFTPError(sftp.FX_FAILURE, "foo"))))
233        d.addCallback(lambda ign:
234            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "_raise_error NoSuchChildError",
235                                         sftpd._raise_error, Failure(NoSuchChildError("foo"))))
236        d.addCallback(lambda ign:
237            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "_raise_error ExistingChildError",
238                                         sftpd._raise_error, Failure(ExistingChildError("foo"))))
239        d.addCallback(lambda ign:
240            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "_raise_error NotWriteableError",
241                                         sftpd._raise_error, Failure(NotWriteableError("foo"))))
242        d.addCallback(lambda ign:
243            self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "_raise_error NotImplementedError",
244                                         sftpd._raise_error, Failure(NotImplementedError("foo"))))
245        d.addCallback(lambda ign:
246            self.shouldFailWithSFTPError(sftp.FX_EOF, "_raise_error EOFError",
247                                         sftpd._raise_error, Failure(EOFError("foo"))))
248        d.addCallback(lambda ign:
249            self.shouldFailWithSFTPError(sftp.FX_EOF, "_raise_error defer.FirstError",
250                                         sftpd._raise_error, Failure(defer.FirstError(
251                                                               Failure(sftp.SFTPError(sftp.FX_EOF, "foo")), 0))))
252        d.addCallback(lambda ign:
253            self.shouldFailWithSFTPError(sftp.FX_FAILURE, "_raise_error AssertionError",
254                                         sftpd._raise_error, Failure(AssertionError("foo"))))
255
256        return d
257
258    def test_not_implemented(self):
259        d = self._set_up("not_implemented")
260
261        d.addCallback(lambda ign:
262            self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "readLink link",
263                                         self.handler.readLink, "link"))
264        d.addCallback(lambda ign:
265            self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "makeLink link file",
266                                         self.handler.makeLink, "link", "file"))
267        d.addCallback(lambda ign:
268            self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "extendedRequest foo bar",
269                                         self.handler.extendedRequest, "foo", "bar"))
270
271        return d
272
273    def _compareDirLists(self, actual, expected):
274       actual_list = sorted(actual)
275       expected_list = sorted(expected)
276       self.failUnlessEqual(len(actual_list), len(expected_list),
277                            "%r is wrong length, expecting %r" % (actual_list, expected_list))
278       for (a, b) in zip(actual_list, expected_list):
279           (name, text, attrs) = a
280           (expected_name, expected_text_re, expected_attrs) = b
281           self.failUnlessEqual(name, expected_name)
282           self.failUnless(re.match(expected_text_re, text), "%r does not match %r" % (text, expected_text_re))
283           # it is ok for there to be extra actual attributes
284           # TODO: check times
285           for e in expected_attrs:
286               self.failUnlessEqual(attrs[e], expected_attrs[e])
287
288    def test_openDirectory_and_attrs(self):
289        d = self._set_up("openDirectory")
290        d.addCallback(lambda ign: self._set_up_tree())
291
292        d.addCallback(lambda ign:
293            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openDirectory small",
294                                         self.handler.openDirectory, "small"))
295        d.addCallback(lambda ign:
296            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openDirectory unknown",
297                                         self.handler.openDirectory, "unknown"))
298        d.addCallback(lambda ign:
299            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openDirectory nodir",
300                                         self.handler.openDirectory, "nodir"))
301        d.addCallback(lambda ign:
302            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openDirectory nodir/nodir",
303                                         self.handler.openDirectory, "nodir/nodir"))
304
305        gross = u"gro\u00DF".encode("utf-8")
306        expected_root = [
307            ('empty_lit_dir', r'drwxrwx--- .* \? .* empty_lit_dir$', {'permissions': S_IFDIR | 0770}),
308            (gross,           r'-rw-rw---- .* 1010 .* '+gross+'$',   {'permissions': S_IFREG | 0660, 'size': 1010}),
309            ('loop',          r'drwxrwx--- .* \? .* loop$',          {'permissions': S_IFDIR | 0770}),
310            ('mutable',       r'-rw-rw---- .* \? .* mutable$',       {'permissions': S_IFREG | 0660}),
311            ('readonly',      r'-r--r----- .* \? .* readonly$',      {'permissions': S_IFREG | 0440}),
312            ('small',         r'-rw-rw---- .* 10 .* small$',         {'permissions': S_IFREG | 0660, 'size': 10}),
313            ('small2',        r'-rw-rw---- .* 26 .* small2$',        {'permissions': S_IFREG | 0660, 'size': 26}),
314            ('tiny_lit_dir',  r'drwxrwx--- .* \? .* tiny_lit_dir$',  {'permissions': S_IFDIR | 0770}),
315            ('unknown',       r'\?--------- .* \? .* unknown$',      {'permissions': 0}),
316        ]
317
318        d.addCallback(lambda ign: self.handler.openDirectory(""))
319        d.addCallback(lambda res: self._compareDirLists(res, expected_root))
320
321        d.addCallback(lambda ign: self.handler.openDirectory("loop"))
322        d.addCallback(lambda res: self._compareDirLists(res, expected_root))
323
324        d.addCallback(lambda ign: self.handler.openDirectory("loop/loop"))
325        d.addCallback(lambda res: self._compareDirLists(res, expected_root))
326
327        d.addCallback(lambda ign: self.handler.openDirectory("empty_lit_dir"))
328        d.addCallback(lambda res: self._compareDirLists(res, []))
329       
330        expected_tiny_lit = [
331            ('short', r'-r--r----- .* 8 Jan 01  1970 short$', {'permissions': S_IFREG | 0440, 'size': 8}),
332        ]
333
334        d.addCallback(lambda ign: self.handler.openDirectory("tiny_lit_dir"))
335        d.addCallback(lambda res: self._compareDirLists(res, expected_tiny_lit))
336
337        d.addCallback(lambda ign: self.handler.getAttrs("small", True))
338        def _check_attrs(attrs):
339            self.failUnlessEqual(attrs['permissions'], S_IFREG | 0440) #FIXME
340            self.failUnlessEqual(attrs['size'], 10)
341        d.addCallback(_check_attrs)
342
343        d.addCallback(lambda ign:
344            self.failUnlessEqual(self.handler.setAttrs("small", {}), None))
345
346        d.addCallback(lambda ign:
347            self.shouldFailWithSFTPError(sftp.FX_OP_UNSUPPORTED, "setAttrs size",
348                                         self.handler.setAttrs, "small", {'size': 0}))
349
350        return d
351
352    def test_openFile_read(self):
353        d = self._set_up("openFile")
354        d.addCallback(lambda ign: self._set_up_tree())
355
356        d.addCallback(lambda ign:
357            self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "openFile small 0",
358                                         self.handler.openFile, "small", 0, {}))
359
360        # attempting to open a non-existent file should fail
361        d.addCallback(lambda ign:
362            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile nofile READ",
363                                         self.handler.openFile, "nofile", sftp.FXF_READ, {}))
364        d.addCallback(lambda ign:
365            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile nodir/file READ",
366                                         self.handler.openFile, "nodir/file", sftp.FXF_READ, {}))
367
368        d.addCallback(lambda ign:
369            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown READ denied",
370                                         self.handler.openFile, "unknown", sftp.FXF_READ, {}))
371        d.addCallback(lambda ign:
372            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir READ denied",
373                                         self.handler.openFile, "tiny_lit_dir", sftp.FXF_READ, {}))
374        d.addCallback(lambda ign:
375            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown uri READ denied",
376                                         self.handler.openFile, "uri/"+self.unknown_uri, sftp.FXF_READ, {}))
377        d.addCallback(lambda ign:
378            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir uri READ denied",
379                                         self.handler.openFile, "uri/"+self.tiny_lit_dir_uri, sftp.FXF_READ, {}))
380
381        # reading an existing file should succeed
382        d.addCallback(lambda ign: self.handler.openFile("small", sftp.FXF_READ, {}))
383        def _read_small(rf):
384            d2 = rf.readChunk(0, 10)
385            d2.addCallback(lambda data: self.failUnlessEqual(data, "0123456789"))
386
387            d2.addCallback(lambda ign: rf.readChunk(2, 6))
388            d2.addCallback(lambda data: self.failUnlessEqual(data, "234567"))
389
390            d2.addCallback(lambda ign: rf.readChunk(8, 4))  # read that starts before EOF is OK
391            d2.addCallback(lambda data: self.failUnlessEqual(data, "89"))
392
393            d2.addCallback(lambda ign:
394                self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting at EOF (0-byte)",
395                                             rf.readChunk, 10, 0))
396            d2.addCallback(lambda ign:
397                self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting at EOF",
398                                             rf.readChunk, 10, 1))
399            d2.addCallback(lambda ign:
400                self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting after EOF",
401                                             rf.readChunk, 11, 1))
402
403            d2.addCallback(lambda ign: rf.getAttrs())
404            def _check_attrs(attrs):
405                self.failUnlessEqual(attrs['permissions'], S_IFREG | 0440) #FIXME
406                self.failUnlessEqual(attrs['size'], 10)
407            d2.addCallback(_check_attrs)
408
409            d2.addCallback(lambda ign:
410                self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "writeChunk on read-only handle denied",
411                                             rf.writeChunk, 0, "a"))
412            d2.addCallback(lambda ign:
413                self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "setAttrs on read-only handle denied",
414                                             rf.setAttrs, {}))
415
416            d2.addCallback(lambda ign: rf.close())
417
418            d2.addCallback(lambda ign:
419                self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "readChunk on closed file",
420                                             rf.readChunk, 0, 1))
421            d2.addCallback(lambda ign:
422                self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "getAttrs on closed file",
423                                             rf.getAttrs))
424
425            d2.addCallback(lambda ign: rf.close()) # should be no-op
426            return d2
427        d.addCallback(_read_small)
428
429        # repeat for a large file
430        gross = u"gro\u00DF".encode("utf-8")
431        d.addCallback(lambda ign: self.handler.openFile(gross, sftp.FXF_READ, {}))
432        def _read_gross(rf):
433            d2 = rf.readChunk(0, 10)
434            d2.addCallback(lambda data: self.failUnlessEqual(data, "0123456789"))
435
436            d2.addCallback(lambda ign: rf.readChunk(2, 6))
437            d2.addCallback(lambda data: self.failUnlessEqual(data, "234567"))
438
439            d2.addCallback(lambda ign: rf.readChunk(1008, 4))  # read that starts before EOF is OK
440            d2.addCallback(lambda data: self.failUnlessEqual(data, "89"))
441
442            d2.addCallback(lambda ign:
443                self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting at EOF (0-byte)",
444                                             rf.readChunk, 1010, 0))
445            d2.addCallback(lambda ign:
446                self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting at EOF",
447                                             rf.readChunk, 1010, 1))
448            d2.addCallback(lambda ign:
449                self.shouldFailWithSFTPError(sftp.FX_EOF, "readChunk starting after EOF",
450                                             rf.readChunk, 1011, 1))
451
452            d2.addCallback(lambda ign: rf.getAttrs())
453            def _check_attrs(attrs):
454                self.failUnlessEqual(attrs['permissions'], S_IFREG | 0440) #FIXME
455                self.failUnlessEqual(attrs['size'], 1010)
456            d2.addCallback(_check_attrs)
457
458            d2.addCallback(lambda ign:
459                self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "writeChunk on read-only handle denied",
460                                             rf.writeChunk, 0, "a"))
461            d2.addCallback(lambda ign:
462                self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "setAttrs on read-only handle denied",
463                                             rf.setAttrs, {}))
464
465            d2.addCallback(lambda ign: rf.close())
466
467            d2.addCallback(lambda ign:
468                self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "readChunk on closed file",
469                                             rf.readChunk, 0, 1))
470            d2.addCallback(lambda ign:
471                self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "getAttrs on closed file",
472                                             rf.getAttrs))
473
474            d2.addCallback(lambda ign: rf.close()) # should be no-op
475            return d2
476        d.addCallback(_read_gross)
477
478        # reading an existing small file via uri/ should succeed
479        d.addCallback(lambda ign: self.handler.openFile("uri/"+self.small_uri, sftp.FXF_READ, {}))
480        def _read_small_uri(rf):
481            d2 = rf.readChunk(0, 10)
482            d2.addCallback(lambda data: self.failUnlessEqual(data, "0123456789"))
483            d2.addCallback(lambda ign: rf.close())
484            return d2
485        d.addCallback(_read_small_uri)
486
487        # repeat for a large file
488        d.addCallback(lambda ign: self.handler.openFile("uri/"+self.gross_uri, sftp.FXF_READ, {}))
489        def _read_gross_uri(rf):
490            d2 = rf.readChunk(0, 10)
491            d2.addCallback(lambda data: self.failUnlessEqual(data, "0123456789"))
492            d2.addCallback(lambda ign: rf.close())
493            return d2
494        d.addCallback(_read_gross_uri)
495
496        # repeat for a mutable file
497        d.addCallback(lambda ign: self.handler.openFile("uri/"+self.mutable_uri, sftp.FXF_READ, {}))
498        def _read_mutable_uri(rf):
499            d2 = rf.readChunk(0, 100)
500            d2.addCallback(lambda data: self.failUnlessEqual(data, "mutable file contents"))
501            d2.addCallback(lambda ign: rf.close())
502            return d2
503        d.addCallback(_read_mutable_uri)
504
505        return d
506
507    def test_openFile_write(self):
508        d = self._set_up("openFile")
509        d.addCallback(lambda ign: self._set_up_tree())
510
511        d.addCallback(lambda ign:
512            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "openFile '' WRITE|CREAT|TRUNC",
513                                             self.handler.openFile, "", sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
514        d.addCallback(lambda ign:
515            self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "openFile newfile WRITE|TRUNC",
516                                         self.handler.openFile, "newfile", sftp.FXF_WRITE | sftp.FXF_TRUNC, {}))
517        d.addCallback(lambda ign:
518            self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "openFile small WRITE|EXCL",
519                                         self.handler.openFile, "small", sftp.FXF_WRITE | sftp.FXF_EXCL, {}))
520        d.addCallback(lambda ign:
521            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir WRITE",
522                                         self.handler.openFile, "tiny_lit_dir", sftp.FXF_WRITE, {}))
523        d.addCallback(lambda ign:
524            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile unknown WRITE",
525                                         self.handler.openFile, "unknown", sftp.FXF_WRITE, {}))
526        d.addCallback(lambda ign:
527            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/newfile WRITE|CREAT|TRUNC",
528                                         self.handler.openFile, "tiny_lit_dir/newfile",
529                                         sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
530        d.addCallback(lambda ign:
531            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/short WRITE",
532                                         self.handler.openFile, "tiny_lit_dir/short", sftp.FXF_WRITE, {}))
533        d.addCallback(lambda ign:
534            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile tiny_lit_dir/short WRITE|CREAT|EXCL",
535                                         self.handler.openFile, "tiny_lit_dir/short",
536                                         sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
537        d.addCallback(lambda ign:
538            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile readonly WRITE",
539                                         self.handler.openFile, "readonly", sftp.FXF_WRITE, {}))
540        d.addCallback(lambda ign:
541            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small WRITE|CREAT|EXCL",
542                                         self.handler.openFile, "small",
543                                         sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
544        d.addCallback(lambda ign:
545            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile readonly uri WRITE",
546                                         self.handler.openFile, "uri/"+self.readonly_uri, sftp.FXF_WRITE, {}))
547        d.addCallback(lambda ign:
548            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE",
549                                         self.handler.openFile, "uri/"+self.small_uri, sftp.FXF_WRITE, {}))
550        d.addCallback(lambda ign:
551            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile small uri WRITE|CREAT|TRUNC",
552                                         self.handler.openFile, "uri/"+self.small_uri,
553                                         sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
554        d.addCallback(lambda ign:
555            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "openFile mutable uri WRITE|CREAT|EXCL",
556                                         self.handler.openFile, "uri/"+self.mutable_uri,
557                                         sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_EXCL, {}))
558
559        d.addCallback(lambda ign:
560                      self.handler.openFile("newfile", sftp.FXF_WRITE | sftp.FXF_CREAT | sftp.FXF_TRUNC, {}))
561        def _write(wf):
562            d2 = wf.writeChunk(0, "0123456789")
563            d2.addCallback(lambda res: self.failUnlessEqual(res, None))
564
565            d2.addCallback(lambda ign: wf.writeChunk(8, "0123"))
566            d2.addCallback(lambda ign: wf.writeChunk(13, "abc"))
567
568            d2.addCallback(lambda ign: wf.getAttrs())
569            def _check_attrs(attrs):
570                self.failUnlessEqual(attrs['permissions'], S_IFREG | 0440) #FIXME
571                self.failUnlessEqual(attrs['size'], 16)
572            d2.addCallback(_check_attrs)
573
574            d2.addCallback(lambda ign: wf.setAttrs({}))
575
576            d2.addCallback(lambda ign:
577                self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "setAttrs with negative size",
578                                             wf.setAttrs, {'size': -1}))
579
580            d2.addCallback(lambda ign: wf.setAttrs({'size': 14}))
581            d2.addCallback(lambda ign: wf.getAttrs())
582            d2.addCallback(lambda attrs: self.failUnlessEqual(attrs['size'], 14))
583
584            d2.addCallback(lambda ign:
585                self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "readChunk on write-only handle denied",
586                                             wf.readChunk, 0, 1))
587
588            d2.addCallback(lambda ign: wf.close())
589
590            d2.addCallback(lambda ign:
591                self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "writeChunk on closed file",
592                                             wf.writeChunk, 0, "a"))
593            d2.addCallback(lambda ign:
594                self.shouldFailWithSFTPError(sftp.FX_BAD_MESSAGE, "setAttrs on closed file",
595                                             wf.setAttrs, {'size': 0}))
596
597            d2.addCallback(lambda ign: wf.close()) # should be no-op
598            return d2
599        d.addCallback(_write)
600        d.addCallback(lambda ign: self.root.get(u"newfile"))
601        d.addCallback(lambda node: download_to_data(node))
602        d.addCallback(lambda data: self.failUnlessEqual(data, "012345670123\x00a"))
603
604        # test APPEND flag, and also replacing an existing file ("newfile")
605        d.addCallback(lambda ign:
606                      self.handler.openFile("newfile", sftp.FXF_WRITE | sftp.FXF_CREAT |
607                                                       sftp.FXF_TRUNC | sftp.FXF_APPEND, {}))
608        def _write_append(wf):
609            d2 = wf.writeChunk(0, "0123456789")
610            d2.addCallback(lambda ign: wf.writeChunk(8, "0123"))
611            d2.addCallback(lambda ign: wf.close())
612            return d2
613        d.addCallback(_write_append)
614        d.addCallback(lambda ign: self.root.get(u"newfile"))
615        d.addCallback(lambda node: download_to_data(node))
616        d.addCallback(lambda data: self.failUnlessEqual(data, "01234567890123"))
617
618        # test EXCL flag
619        d.addCallback(lambda ign:
620                      self.handler.openFile("excl", sftp.FXF_WRITE | sftp.FXF_CREAT |
621                                                    sftp.FXF_TRUNC | sftp.FXF_EXCL, {}))
622        def _write_excl(wf):
623            d2 = self.root.get(u"excl")
624            d2.addCallback(lambda node: download_to_data(node))
625            d2.addCallback(lambda data: self.failUnlessEqual(data, ""))
626
627            d2.addCallback(lambda ign: wf.writeChunk(0, "0123456789"))
628            d2.addCallback(lambda ign: wf.close())
629            return d2
630        d.addCallback(_write_excl)
631        d.addCallback(lambda ign: self.root.get(u"excl"))
632        d.addCallback(lambda node: download_to_data(node))
633        d.addCallback(lambda data: self.failUnlessEqual(data, "0123456789"))
634
635        # test WRITE | CREAT without TRUNC
636        d.addCallback(lambda ign:
637                      self.handler.openFile("newfile2", sftp.FXF_WRITE | sftp.FXF_CREAT, {}))
638        def _write_notrunc(wf):
639            d2 =  wf.writeChunk(0, "0123456789")
640            d2.addCallback(lambda ign: wf.close())
641            return d2
642        d.addCallback(_write_notrunc)
643        d.addCallback(lambda ign: self.root.get(u"newfile2"))
644        d.addCallback(lambda node: download_to_data(node))
645        d.addCallback(lambda data: self.failUnlessEqual(data, "0123456789"))
646
647        # test writing to a mutable file
648        d.addCallback(lambda ign:
649                      self.handler.openFile("mutable", sftp.FXF_WRITE, {}))
650        def _write_mutable(wf):
651            d2 = wf.writeChunk(8, "new!")
652            d2.addCallback(lambda ign: wf.close())
653            return d2
654        d.addCallback(_write_mutable)
655        d.addCallback(lambda ign: self.root.get(u"mutable"))
656        def _check_same_file(node):
657            self.failUnless(node.is_mutable())
658            self.failUnlessEqual(node.get_uri(), self.mutable_uri)
659            return node.download_best_version()
660        d.addCallback(_check_same_file)
661        d.addCallback(lambda data: self.failUnlessEqual(data, "mutable new! contents"))
662
663        """
664        # test READ | WRITE without CREAT or TRUNC
665        d.addCallback(lambda ign:
666                      self.handler.openFile("small", sftp.FXF_READ | sftp.FXF_WRITE, {}))
667        def _read_write(rwf):
668            d2 =  rwf.writeChunk(8, "0123")
669            d2.addCallback(lambda ign: rwf.readChunk(0, 100))
670            d2.addCallback(lambda data: self.failUnlessEqual(data, "012345670123"))
671            d2.addCallback(lambda ign: rwf.close())
672            return d2
673        d.addCallback(_read_write)
674        d.addCallback(lambda ign: self.root.get(u"small"))
675        d.addCallback(lambda node: download_to_data(node))
676        d.addCallback(lambda data: self.failUnlessEqual(data, "012345670123"))
677        """
678        return d
679
680    def test_removeFile(self):
681        d = self._set_up("removeFile")
682        d.addCallback(lambda ign: self._set_up_tree())
683
684        d.addCallback(lambda ign:
685            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeFile nofile",
686                                         self.handler.removeFile, "nofile"))
687        d.addCallback(lambda ign:
688            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeFile nofile",
689                                         self.handler.removeFile, "nofile"))
690        d.addCallback(lambda ign:
691            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeFile nodir/file",
692                                         self.handler.removeFile, "nodir/file"))
693        d.addCallback(lambda ign:
694            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removefile ''",
695                                         self.handler.removeFile, ""))
696           
697        # removing a directory should fail
698        d.addCallback(lambda ign:
699            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "removeFile tiny_lit_dir",
700                                         self.handler.removeFile, "tiny_lit_dir"))
701
702        # removing a file should succeed
703        d.addCallback(lambda ign: self.root.get(u"gro\u00DF"))
704        d.addCallback(lambda ign: self.handler.removeFile(u"gro\u00DF".encode('utf-8')))
705        d.addCallback(lambda ign:
706                      self.shouldFail(NoSuchChildError, "removeFile gross", "gro\\xdf",
707                                      self.root.get, u"gro\u00DF"))
708
709        # removing an unknown should succeed
710        d.addCallback(lambda ign: self.root.get(u"unknown"))
711        d.addCallback(lambda ign: self.handler.removeFile("unknown"))
712        d.addCallback(lambda ign:
713                      self.shouldFail(NoSuchChildError, "removeFile unknown", "unknown",
714                                      self.root.get, u"unknown"))
715
716        # removing a link to an open file should not prevent it from being read
717        d.addCallback(lambda ign: self.handler.openFile("small", sftp.FXF_READ, {}))
718        def _remove_and_read_small(rf):
719            d2= self.handler.removeFile("small")
720            d2.addCallback(lambda ign:
721                           self.shouldFail(NoSuchChildError, "removeFile small", "small",
722                                           self.root.get, u"small"))
723            d2.addCallback(lambda ign: rf.readChunk(0, 10))
724            d2.addCallback(lambda data: self.failUnlessEqual(data, "0123456789"))
725            d2.addCallback(lambda ign: rf.close())
726            return d2
727        d.addCallback(_remove_and_read_small)
728
729        return d
730
731    def test_removeDirectory(self):
732        d = self._set_up("removeDirectory")
733        d.addCallback(lambda ign: self._set_up_tree())
734
735        d.addCallback(lambda ign:
736            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeDirectory nodir",
737                                         self.handler.removeDirectory, "nodir"))
738        d.addCallback(lambda ign:
739            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeDirectory nodir/nodir",
740                                         self.handler.removeDirectory, "nodir/nodir"))
741        d.addCallback(lambda ign:
742            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "removeDirectory ''",
743                                         self.handler.removeDirectory, ""))
744
745        # removing a file should fail
746        d.addCallback(lambda ign:
747            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "removeDirectory gross",
748                                         self.handler.removeDirectory, u"gro\u00DF".encode('utf-8')))
749
750        # removing a directory should succeed
751        d.addCallback(lambda ign: self.root.get(u"tiny_lit_dir"))
752        d.addCallback(lambda ign: self.handler.removeDirectory("tiny_lit_dir"))
753        d.addCallback(lambda ign:
754                      self.shouldFail(NoSuchChildError, "removeDirectory tiny_lit_dir", "tiny_lit_dir",
755                                      self.root.get, u"tiny_lit_dir"))
756
757        # removing an unknown should succeed
758        d.addCallback(lambda ign: self.root.get(u"unknown"))
759        d.addCallback(lambda ign: self.handler.removeDirectory("unknown"))
760        d.addCallback(lambda err:
761                      self.shouldFail(NoSuchChildError, "removeDirectory unknown", "unknown",
762                                      self.root.get, u"unknown"))
763
764        return d
765
766    def test_renameFile(self):
767        d = self._set_up("renameFile")
768        d.addCallback(lambda ign: self._set_up_tree())
769
770        # renaming a non-existent file should fail
771        d.addCallback(lambda ign:
772            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile nofile newfile",
773                                         self.handler.renameFile, "nofile", "newfile"))
774        d.addCallback(lambda ign:
775            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile '' newfile",
776                                         self.handler.renameFile, "", "newfile"))
777
778        # renaming a file to a non-existent path should fail
779        d.addCallback(lambda ign:
780            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small nodir/small",
781                                         self.handler.renameFile, "small", "nodir/small"))
782
783        # renaming a file to an invalid UTF-8 name should fail
784        d.addCallback(lambda ign:
785            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small invalid",
786                                         self.handler.renameFile, "small", "\xFF"))
787
788        # renaming a file to or from an URI should fail
789        d.addCallback(lambda ign:
790            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small from uri",
791                                         self.handler.renameFile, "uri/"+self.small_uri, "new"))
792        d.addCallback(lambda ign:
793            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "renameFile small to uri",
794                                         self.handler.renameFile, "small", "uri/fake_uri"))
795
796        # renaming a file onto an existing file, directory or unknown should fail
797        d.addCallback(lambda ign:
798            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small small2",
799                                         self.handler.renameFile, "small", "small2"))
800        d.addCallback(lambda ign:
801            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small tiny_lit_dir",
802                                         self.handler.renameFile, "small", "tiny_lit_dir"))
803        d.addCallback(lambda ign:
804            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "renameFile small unknown",
805                                         self.handler.renameFile, "small", "unknown"))
806
807        # renaming a file to a correct path should succeed
808        d.addCallback(lambda ign: self.handler.renameFile("small", "new_small"))
809        d.addCallback(lambda ign: self.root.get(u"new_small"))
810        d.addCallback(lambda node: self.failUnlessEqual(node.get_uri(), self.small_uri))
811
812        # renaming a file into a subdirectory should succeed (also tests Unicode names)
813        d.addCallback(lambda ign: self.handler.renameFile(u"gro\u00DF".encode('utf-8'),
814                                                          u"loop/neue_gro\u00DF".encode('utf-8')))
815        d.addCallback(lambda ign: self.root.get(u"neue_gro\u00DF"))
816        d.addCallback(lambda node: self.failUnlessEqual(node.get_uri(), self.gross_uri))
817
818        # renaming a directory to a correct path should succeed
819        d.addCallback(lambda ign: self.handler.renameFile("tiny_lit_dir", "new_tiny_lit_dir"))
820        d.addCallback(lambda ign: self.root.get(u"new_tiny_lit_dir"))
821        d.addCallback(lambda node: self.failUnlessEqual(node.get_uri(), self.tiny_lit_dir_uri))
822
823        # renaming an unknown to a correct path should succeed
824        d.addCallback(lambda ign: self.handler.renameFile("unknown", "new_unknown"))
825        d.addCallback(lambda ign: self.root.get(u"new_unknown"))
826        d.addCallback(lambda node: self.failUnlessEqual(node.get_uri(), self.unknown_uri))
827
828        return d
829
830    def test_makeDirectory(self):
831        d = self._set_up("makeDirectory")
832        d.addCallback(lambda ign: self._set_up_tree())
833           
834        # making a directory at a correct path should succeed
835        d.addCallback(lambda ign: self.handler.makeDirectory("newdir", {'ext_foo': 'bar', 'ctime': 42}))
836
837        d.addCallback(lambda ign: self.root.get_child_and_metadata(u"newdir"))
838        def _got( (child, metadata) ):
839            self.failUnless(IDirectoryNode.providedBy(child))
840            self.failUnless(child.is_mutable())
841            # FIXME
842            #self.failUnless('ctime' in metadata, metadata)
843            #self.failUnlessEqual(metadata['ctime'], 42)
844            #self.failUnless('ext_foo' in metadata, metadata)
845            #self.failUnlessEqual(metadata['ext_foo'], 'bar')
846            # TODO: child should be empty
847        d.addCallback(_got)
848
849        # making intermediate directories should also succeed
850        d.addCallback(lambda ign: self.handler.makeDirectory("newparent/newchild", {}))
851
852        d.addCallback(lambda ign: self.root.get(u"newparent"))
853        def _got_newparent(newparent):
854            self.failUnless(IDirectoryNode.providedBy(newparent))
855            self.failUnless(newparent.is_mutable())
856            return newparent.get(u"newchild")
857        d.addCallback(_got_newparent)
858
859        def _got_newchild(newchild):
860            self.failUnless(IDirectoryNode.providedBy(newchild))
861            self.failUnless(newchild.is_mutable())
862        d.addCallback(_got_newchild)
863
864        d.addCallback(lambda ign:
865            self.shouldFailWithSFTPError(sftp.FX_NO_SUCH_FILE, "makeDirectory invalid UTF-8",
866                                         self.handler.makeDirectory, "\xFF", {}))
867
868        # should fail because there is an existing file "small"
869        d.addCallback(lambda ign:
870            self.shouldFailWithSFTPError(sftp.FX_PERMISSION_DENIED, "makeDirectory small",
871                                         self.handler.makeDirectory, "small", {}))
872        return d