Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / conch / test / test_filetransfer.py
1 # -*- test-case-name: twisted.conch.test.test_filetransfer -*-
2 # Copyright (c) 2001-2008 Twisted Matrix Laboratories.
3 # See LICENSE file for details.
4
5
6 import os
7 import re
8 import struct
9 import sys
10
11 from twisted.trial import unittest
12 try:
13     from twisted.conch import unix
14     unix # shut up pyflakes
15 except ImportError:
16     unix = None
17     try:
18         del sys.modules['twisted.conch.unix'] # remove the bad import
19     except KeyError:
20         # In Python 2.4, the bad import has already been cleaned up for us.
21         # Hooray.
22         pass
23
24 from twisted.conch import avatar
25 from twisted.conch.ssh import common, connection, filetransfer, session
26 from twisted.internet import defer
27 from twisted.protocols import loopback
28 from twisted.python import components
29
30
31 class TestAvatar(avatar.ConchUser):
32     def __init__(self):
33         avatar.ConchUser.__init__(self)
34         self.channelLookup['session'] = session.SSHSession
35         self.subsystemLookup['sftp'] = filetransfer.FileTransferServer
36
37     def _runAsUser(self, f, *args, **kw):
38         try:
39             f = iter(f)
40         except TypeError:
41             f = [(f, args, kw)]
42         for i in f:
43             func = i[0]
44             args = len(i)>1 and i[1] or ()
45             kw = len(i)>2 and i[2] or {}
46             r = func(*args, **kw)
47         return r
48
49
50 class FileTransferTestAvatar(TestAvatar):
51
52     def __init__(self, homeDir):
53         TestAvatar.__init__(self)
54         self.homeDir = homeDir
55
56     def getHomeDir(self):
57         return os.path.join(os.getcwd(), self.homeDir)
58
59
60 class ConchSessionForTestAvatar:
61
62     def __init__(self, avatar):
63         self.avatar = avatar
64
65 if unix:
66     if not hasattr(unix, 'SFTPServerForUnixConchUser'):
67         # unix should either be a fully working module, or None.  I'm not sure
68         # how this happens, but on win32 it does.  Try to cope.  --spiv.
69         import warnings
70         warnings.warn(("twisted.conch.unix imported %r, "
71                        "but doesn't define SFTPServerForUnixConchUser'")
72                       % (unix,))
73         unix = None
74     else:
75         class FileTransferForTestAvatar(unix.SFTPServerForUnixConchUser):
76
77             def gotVersion(self, version, otherExt):
78                 return {'conchTest' : 'ext data'}
79
80             def extendedRequest(self, extName, extData):
81                 if extName == 'testExtendedRequest':
82                     return 'bar'
83                 raise NotImplementedError
84
85         components.registerAdapter(FileTransferForTestAvatar,
86                                    TestAvatar,
87                                    filetransfer.ISFTPServer)
88
89 class SFTPTestBase(unittest.TestCase):
90
91     def setUp(self):
92         self.testDir = self.mktemp()
93         # Give the testDir another level so we can safely "cd .." from it in
94         # tests.
95         self.testDir = os.path.join(self.testDir, 'extra')
96         os.makedirs(os.path.join(self.testDir, 'testDirectory'))
97
98         f = file(os.path.join(self.testDir, 'testfile1'),'w')
99         f.write('a'*10+'b'*10)
100         f.write(file('/dev/urandom').read(1024*64)) # random data
101         os.chmod(os.path.join(self.testDir, 'testfile1'), 0644)
102         file(os.path.join(self.testDir, 'testRemoveFile'), 'w').write('a')
103         file(os.path.join(self.testDir, 'testRenameFile'), 'w').write('a')
104         file(os.path.join(self.testDir, '.testHiddenFile'), 'w').write('a')
105
106
107 class TestOurServerOurClient(SFTPTestBase):
108
109     if not unix:
110         skip = "can't run on non-posix computers"
111
112     def setUp(self):
113         SFTPTestBase.setUp(self)
114
115         self.avatar = FileTransferTestAvatar(self.testDir)
116         self.server = filetransfer.FileTransferServer(avatar=self.avatar)
117         clientTransport = loopback.LoopbackRelay(self.server)
118
119         self.client = filetransfer.FileTransferClient()
120         self._serverVersion = None
121         self._extData = None
122         def _(serverVersion, extData):
123             self._serverVersion = serverVersion
124             self._extData = extData
125         self.client.gotServerVersion = _
126         serverTransport = loopback.LoopbackRelay(self.client)
127         self.client.makeConnection(clientTransport)
128         self.server.makeConnection(serverTransport)
129
130         self.clientTransport = clientTransport
131         self.serverTransport = serverTransport
132
133         self._emptyBuffers()
134
135
136     def _emptyBuffers(self):
137         while self.serverTransport.buffer or self.clientTransport.buffer:
138             self.serverTransport.clearBuffer()
139             self.clientTransport.clearBuffer()
140
141
142     def tearDown(self):
143         self.serverTransport.loseConnection()
144         self.clientTransport.loseConnection()
145         self.serverTransport.clearBuffer()
146         self.clientTransport.clearBuffer()
147
148
149     def testServerVersion(self):
150         self.assertEqual(self._serverVersion, 3)
151         self.assertEqual(self._extData, {'conchTest' : 'ext data'})
152
153
154     def test_openedFileClosedWithConnection(self):
155         """
156         A file opened with C{openFile} is close when the connection is lost.
157         """
158         d = self.client.openFile("testfile1", filetransfer.FXF_READ |
159                                  filetransfer.FXF_WRITE, {})
160         self._emptyBuffers()
161
162         oldClose = os.close
163         closed = []
164         def close(fd):
165             closed.append(fd)
166             oldClose(fd)
167
168         self.patch(os, "close", close)
169
170         def _fileOpened(openFile):
171             fd = self.server.openFiles[openFile.handle[4:]].fd
172             self.serverTransport.loseConnection()
173             self.clientTransport.loseConnection()
174             self.serverTransport.clearBuffer()
175             self.clientTransport.clearBuffer()
176             self.assertEqual(self.server.openFiles, {})
177             self.assertIn(fd, closed)
178
179         d.addCallback(_fileOpened)
180         return d
181
182
183     def test_openedDirectoryClosedWithConnection(self):
184         """
185         A directory opened with C{openDirectory} is close when the connection
186         is lost.
187         """
188         d = self.client.openDirectory('')
189         self._emptyBuffers()
190
191         def _getFiles(openDir):
192             self.serverTransport.loseConnection()
193             self.clientTransport.loseConnection()
194             self.serverTransport.clearBuffer()
195             self.clientTransport.clearBuffer()
196             self.assertEqual(self.server.openDirs, {})
197
198         d.addCallback(_getFiles)
199         return d
200
201
202     def testOpenFileIO(self):
203         d = self.client.openFile("testfile1", filetransfer.FXF_READ |
204                                  filetransfer.FXF_WRITE, {})
205         self._emptyBuffers()
206
207         def _fileOpened(openFile):
208             self.assertEqual(openFile, filetransfer.ISFTPFile(openFile))
209             d = _readChunk(openFile)
210             d.addCallback(_writeChunk, openFile)
211             return d
212
213         def _readChunk(openFile):
214             d = openFile.readChunk(0, 20)
215             self._emptyBuffers()
216             d.addCallback(self.assertEqual, 'a'*10 + 'b'*10)
217             return d
218
219         def _writeChunk(_, openFile):
220             d = openFile.writeChunk(20, 'c'*10)
221             self._emptyBuffers()
222             d.addCallback(_readChunk2, openFile)
223             return d
224
225         def _readChunk2(_, openFile):
226             d = openFile.readChunk(0, 30)
227             self._emptyBuffers()
228             d.addCallback(self.assertEqual, 'a'*10 + 'b'*10 + 'c'*10)
229             return d
230
231         d.addCallback(_fileOpened)
232         return d
233
234     def testClosedFileGetAttrs(self):
235         d = self.client.openFile("testfile1", filetransfer.FXF_READ |
236                                  filetransfer.FXF_WRITE, {})
237         self._emptyBuffers()
238
239         def _getAttrs(_, openFile):
240             d = openFile.getAttrs()
241             self._emptyBuffers()
242             return d
243
244         def _err(f):
245             self.flushLoggedErrors()
246             return f
247
248         def _close(openFile):
249             d = openFile.close()
250             self._emptyBuffers()
251             d.addCallback(_getAttrs, openFile)
252             d.addErrback(_err)
253             return self.assertFailure(d, filetransfer.SFTPError)
254
255         d.addCallback(_close)
256         return d
257
258     def testOpenFileAttributes(self):
259         d = self.client.openFile("testfile1", filetransfer.FXF_READ |
260                                  filetransfer.FXF_WRITE, {})
261         self._emptyBuffers()
262
263         def _getAttrs(openFile):
264             d = openFile.getAttrs()
265             self._emptyBuffers()
266             d.addCallback(_getAttrs2)
267             return d
268
269         def _getAttrs2(attrs1):
270             d = self.client.getAttrs('testfile1')
271             self._emptyBuffers()
272             d.addCallback(self.assertEqual, attrs1)
273             return d
274
275         return d.addCallback(_getAttrs)
276
277
278     def testOpenFileSetAttrs(self):
279         # XXX test setAttrs
280         # Ok, how about this for a start?  It caught a bug :)  -- spiv.
281         d = self.client.openFile("testfile1", filetransfer.FXF_READ |
282                                  filetransfer.FXF_WRITE, {})
283         self._emptyBuffers()
284
285         def _getAttrs(openFile):
286             d = openFile.getAttrs()
287             self._emptyBuffers()
288             d.addCallback(_setAttrs)
289             return d
290
291         def _setAttrs(attrs):
292             attrs['atime'] = 0
293             d = self.client.setAttrs('testfile1', attrs)
294             self._emptyBuffers()
295             d.addCallback(_getAttrs2)
296             d.addCallback(self.assertEqual, attrs)
297             return d
298
299         def _getAttrs2(_):
300             d = self.client.getAttrs('testfile1')
301             self._emptyBuffers()
302             return d
303
304         d.addCallback(_getAttrs)
305         return d
306
307
308     def test_openFileExtendedAttributes(self):
309         """
310         Check that L{filetransfer.FileTransferClient.openFile} can send
311         extended attributes, that should be extracted server side. By default,
312         they are ignored, so we just verify they are correctly parsed.
313         """
314         savedAttributes = {}
315         oldOpenFile = self.server.client.openFile
316         def openFile(filename, flags, attrs):
317             savedAttributes.update(attrs)
318             return oldOpenFile(filename, flags, attrs)
319         self.server.client.openFile = openFile
320
321         d = self.client.openFile("testfile1", filetransfer.FXF_READ |
322                 filetransfer.FXF_WRITE, {"ext_foo": "bar"})
323         self._emptyBuffers()
324
325         def check(ign):
326             self.assertEqual(savedAttributes, {"ext_foo": "bar"})
327
328         return d.addCallback(check)
329
330
331     def testRemoveFile(self):
332         d = self.client.getAttrs("testRemoveFile")
333         self._emptyBuffers()
334         def _removeFile(ignored):
335             d = self.client.removeFile("testRemoveFile")
336             self._emptyBuffers()
337             return d
338         d.addCallback(_removeFile)
339         d.addCallback(_removeFile)
340         return self.assertFailure(d, filetransfer.SFTPError)
341
342     def testRenameFile(self):
343         d = self.client.getAttrs("testRenameFile")
344         self._emptyBuffers()
345         def _rename(attrs):
346             d = self.client.renameFile("testRenameFile", "testRenamedFile")
347             self._emptyBuffers()
348             d.addCallback(_testRenamed, attrs)
349             return d
350         def _testRenamed(_, attrs):
351             d = self.client.getAttrs("testRenamedFile")
352             self._emptyBuffers()
353             d.addCallback(self.assertEqual, attrs)
354         return d.addCallback(_rename)
355
356     def testDirectoryBad(self):
357         d = self.client.getAttrs("testMakeDirectory")
358         self._emptyBuffers()
359         return self.assertFailure(d, filetransfer.SFTPError)
360
361     def testDirectoryCreation(self):
362         d = self.client.makeDirectory("testMakeDirectory", {})
363         self._emptyBuffers()
364
365         def _getAttrs(_):
366             d = self.client.getAttrs("testMakeDirectory")
367             self._emptyBuffers()
368             return d
369
370         # XXX not until version 4/5
371         # self.assertEqual(filetransfer.FILEXFER_TYPE_DIRECTORY&attrs['type'],
372         #                     filetransfer.FILEXFER_TYPE_DIRECTORY)
373
374         def _removeDirectory(_):
375             d = self.client.removeDirectory("testMakeDirectory")
376             self._emptyBuffers()
377             return d
378
379         d.addCallback(_getAttrs)
380         d.addCallback(_removeDirectory)
381         d.addCallback(_getAttrs)
382         return self.assertFailure(d, filetransfer.SFTPError)
383
384     def testOpenDirectory(self):
385         d = self.client.openDirectory('')
386         self._emptyBuffers()
387         files = []
388
389         def _getFiles(openDir):
390             def append(f):
391                 files.append(f)
392                 return openDir
393             d = defer.maybeDeferred(openDir.next)
394             self._emptyBuffers()
395             d.addCallback(append)
396             d.addCallback(_getFiles)
397             d.addErrback(_close, openDir)
398             return d
399
400         def _checkFiles(ignored):
401             fs = list(zip(*files)[0])
402             fs.sort()
403             self.assertEqual(fs,
404                                  ['.testHiddenFile', 'testDirectory',
405                                   'testRemoveFile', 'testRenameFile',
406                                   'testfile1'])
407
408         def _close(_, openDir):
409             d = openDir.close()
410             self._emptyBuffers()
411             return d
412
413         d.addCallback(_getFiles)
414         d.addCallback(_checkFiles)
415         return d
416
417     def testLinkDoesntExist(self):
418         d = self.client.getAttrs('testLink')
419         self._emptyBuffers()
420         return self.assertFailure(d, filetransfer.SFTPError)
421
422     def testLinkSharesAttrs(self):
423         d = self.client.makeLink('testLink', 'testfile1')
424         self._emptyBuffers()
425         def _getFirstAttrs(_):
426             d = self.client.getAttrs('testLink', 1)
427             self._emptyBuffers()
428             return d
429         def _getSecondAttrs(firstAttrs):
430             d = self.client.getAttrs('testfile1')
431             self._emptyBuffers()
432             d.addCallback(self.assertEqual, firstAttrs)
433             return d
434         d.addCallback(_getFirstAttrs)
435         return d.addCallback(_getSecondAttrs)
436
437     def testLinkPath(self):
438         d = self.client.makeLink('testLink', 'testfile1')
439         self._emptyBuffers()
440         def _readLink(_):
441             d = self.client.readLink('testLink')
442             self._emptyBuffers()
443             d.addCallback(self.assertEqual,
444                           os.path.join(os.getcwd(), self.testDir, 'testfile1'))
445             return d
446         def _realPath(_):
447             d = self.client.realPath('testLink')
448             self._emptyBuffers()
449             d.addCallback(self.assertEqual,
450                           os.path.join(os.getcwd(), self.testDir, 'testfile1'))
451             return d
452         d.addCallback(_readLink)
453         d.addCallback(_realPath)
454         return d
455
456     def testExtendedRequest(self):
457         d = self.client.extendedRequest('testExtendedRequest', 'foo')
458         self._emptyBuffers()
459         d.addCallback(self.assertEqual, 'bar')
460         d.addCallback(self._cbTestExtendedRequest)
461         return d
462
463     def _cbTestExtendedRequest(self, ignored):
464         d = self.client.extendedRequest('testBadRequest', '')
465         self._emptyBuffers()
466         return self.assertFailure(d, NotImplementedError)
467
468
469 class FakeConn:
470     def sendClose(self, channel):
471         pass
472
473
474 class TestFileTransferClose(unittest.TestCase):
475
476     if not unix:
477         skip = "can't run on non-posix computers"
478
479     def setUp(self):
480         self.avatar = TestAvatar()
481
482     def buildServerConnection(self):
483         # make a server connection
484         conn = connection.SSHConnection()
485         # server connections have a 'self.transport.avatar'.
486         class DummyTransport:
487             def __init__(self):
488                 self.transport = self
489             def sendPacket(self, kind, data):
490                 pass
491             def logPrefix(self):
492                 return 'dummy transport'
493         conn.transport = DummyTransport()
494         conn.transport.avatar = self.avatar
495         return conn
496
497     def interceptConnectionLost(self, sftpServer):
498         self.connectionLostFired = False
499         origConnectionLost = sftpServer.connectionLost
500         def connectionLost(reason):
501             self.connectionLostFired = True
502             origConnectionLost(reason)
503         sftpServer.connectionLost = connectionLost
504
505     def assertSFTPConnectionLost(self):
506         self.assertTrue(self.connectionLostFired,
507             "sftpServer's connectionLost was not called")
508
509     def test_sessionClose(self):
510         """
511         Closing a session should notify an SFTP subsystem launched by that
512         session.
513         """
514         # make a session
515         testSession = session.SSHSession(conn=FakeConn(), avatar=self.avatar)
516
517         # start an SFTP subsystem on the session
518         testSession.request_subsystem(common.NS('sftp'))
519         sftpServer = testSession.client.transport.proto
520
521         # intercept connectionLost so we can check that it's called
522         self.interceptConnectionLost(sftpServer)
523
524         # close session
525         testSession.closeReceived()
526
527         self.assertSFTPConnectionLost()
528
529     def test_clientClosesChannelOnConnnection(self):
530         """
531         A client sending CHANNEL_CLOSE should trigger closeReceived on the
532         associated channel instance.
533         """
534         conn = self.buildServerConnection()
535
536         # somehow get a session
537         packet = common.NS('session') + struct.pack('>L', 0) * 3
538         conn.ssh_CHANNEL_OPEN(packet)
539         sessionChannel = conn.channels[0]
540
541         sessionChannel.request_subsystem(common.NS('sftp'))
542         sftpServer = sessionChannel.client.transport.proto
543         self.interceptConnectionLost(sftpServer)
544
545         # intercept closeReceived
546         self.interceptConnectionLost(sftpServer)
547
548         # close the connection
549         conn.ssh_CHANNEL_CLOSE(struct.pack('>L', 0))
550
551         self.assertSFTPConnectionLost()
552
553
554     def test_stopConnectionServiceClosesChannel(self):
555         """
556         Closing an SSH connection should close all sessions within it.
557         """
558         conn = self.buildServerConnection()
559
560         # somehow get a session
561         packet = common.NS('session') + struct.pack('>L', 0) * 3
562         conn.ssh_CHANNEL_OPEN(packet)
563         sessionChannel = conn.channels[0]
564
565         sessionChannel.request_subsystem(common.NS('sftp'))
566         sftpServer = sessionChannel.client.transport.proto
567         self.interceptConnectionLost(sftpServer)
568
569         # close the connection
570         conn.serviceStopped()
571
572         self.assertSFTPConnectionLost()
573
574
575
576 class TestConstants(unittest.TestCase):
577     """
578     Tests for the constants used by the SFTP protocol implementation.
579
580     @ivar filexferSpecExcerpts: Excerpts from the
581         draft-ietf-secsh-filexfer-02.txt (draft) specification of the SFTP
582         protocol.  There are more recent drafts of the specification, but this
583         one describes version 3, which is what conch (and OpenSSH) implements.
584     """
585
586
587     filexferSpecExcerpts = [
588         """
589            The following values are defined for packet types.
590
591                 #define SSH_FXP_INIT                1
592                 #define SSH_FXP_VERSION             2
593                 #define SSH_FXP_OPEN                3
594                 #define SSH_FXP_CLOSE               4
595                 #define SSH_FXP_READ                5
596                 #define SSH_FXP_WRITE               6
597                 #define SSH_FXP_LSTAT               7
598                 #define SSH_FXP_FSTAT               8
599                 #define SSH_FXP_SETSTAT             9
600                 #define SSH_FXP_FSETSTAT           10
601                 #define SSH_FXP_OPENDIR            11
602                 #define SSH_FXP_READDIR            12
603                 #define SSH_FXP_REMOVE             13
604                 #define SSH_FXP_MKDIR              14
605                 #define SSH_FXP_RMDIR              15
606                 #define SSH_FXP_REALPATH           16
607                 #define SSH_FXP_STAT               17
608                 #define SSH_FXP_RENAME             18
609                 #define SSH_FXP_READLINK           19
610                 #define SSH_FXP_SYMLINK            20
611                 #define SSH_FXP_STATUS            101
612                 #define SSH_FXP_HANDLE            102
613                 #define SSH_FXP_DATA              103
614                 #define SSH_FXP_NAME              104
615                 #define SSH_FXP_ATTRS             105
616                 #define SSH_FXP_EXTENDED          200
617                 #define SSH_FXP_EXTENDED_REPLY    201
618
619            Additional packet types should only be defined if the protocol
620            version number (see Section ``Protocol Initialization'') is
621            incremented, and their use MUST be negotiated using the version
622            number.  However, the SSH_FXP_EXTENDED and SSH_FXP_EXTENDED_REPLY
623            packets can be used to implement vendor-specific extensions.  See
624            Section ``Vendor-Specific-Extensions'' for more details.
625         """,
626         """
627             The flags bits are defined to have the following values:
628
629                 #define SSH_FILEXFER_ATTR_SIZE          0x00000001
630                 #define SSH_FILEXFER_ATTR_UIDGID        0x00000002
631                 #define SSH_FILEXFER_ATTR_PERMISSIONS   0x00000004
632                 #define SSH_FILEXFER_ATTR_ACMODTIME     0x00000008
633                 #define SSH_FILEXFER_ATTR_EXTENDED      0x80000000
634
635         """,
636         """
637             The `pflags' field is a bitmask.  The following bits have been
638            defined.
639
640                 #define SSH_FXF_READ            0x00000001
641                 #define SSH_FXF_WRITE           0x00000002
642                 #define SSH_FXF_APPEND          0x00000004
643                 #define SSH_FXF_CREAT           0x00000008
644                 #define SSH_FXF_TRUNC           0x00000010
645                 #define SSH_FXF_EXCL            0x00000020
646         """,
647         """
648             Currently, the following values are defined (other values may be
649            defined by future versions of this protocol):
650
651                 #define SSH_FX_OK                            0
652                 #define SSH_FX_EOF                           1
653                 #define SSH_FX_NO_SUCH_FILE                  2
654                 #define SSH_FX_PERMISSION_DENIED             3
655                 #define SSH_FX_FAILURE                       4
656                 #define SSH_FX_BAD_MESSAGE                   5
657                 #define SSH_FX_NO_CONNECTION                 6
658                 #define SSH_FX_CONNECTION_LOST               7
659                 #define SSH_FX_OP_UNSUPPORTED                8
660         """]
661
662
663     def test_constantsAgainstSpec(self):
664         """
665         The constants used by the SFTP protocol implementation match those
666         found by searching through the spec.
667         """
668         constants = {}
669         for excerpt in self.filexferSpecExcerpts:
670             for line in excerpt.splitlines():
671                 m = re.match('^\s*#define SSH_([A-Z_]+)\s+([0-9x]*)\s*$', line)
672                 if m:
673                     constants[m.group(1)] = long(m.group(2), 0)
674         self.assertTrue(
675             len(constants) > 0, "No constants found (the test must be buggy).")
676         for k, v in constants.items():
677             self.assertEqual(v, getattr(filetransfer, k))
678
679
680
681 class TestRawPacketData(unittest.TestCase):
682     """
683     Tests for L{filetransfer.FileTransferClient} which explicitly craft certain
684     less common protocol messages to exercise their handling.
685     """
686     def setUp(self):
687         self.ftc = filetransfer.FileTransferClient()
688
689
690     def test_packetSTATUS(self):
691         """
692         A STATUS packet containing a result code, a message, and a language is
693         parsed to produce the result of an outstanding request L{Deferred}.
694
695         @see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
696             of the SFTP Internet-Draft.
697         """
698         d = defer.Deferred()
699         d.addCallback(self._cbTestPacketSTATUS)
700         self.ftc.openRequests[1] = d
701         data = struct.pack('!LL', 1, filetransfer.FX_OK) + common.NS('msg') + common.NS('lang')
702         self.ftc.packet_STATUS(data)
703         return d
704
705
706     def _cbTestPacketSTATUS(self, result):
707         """
708         Assert that the result is a two-tuple containing the message and
709         language from the STATUS packet.
710         """
711         self.assertEqual(result[0], 'msg')
712         self.assertEqual(result[1], 'lang')
713
714
715     def test_packetSTATUSShort(self):
716         """
717         A STATUS packet containing only a result code can also be parsed to
718         produce the result of an outstanding request L{Deferred}.  Such packets
719         are sent by some SFTP implementations, though not strictly legal.
720
721         @see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
722             of the SFTP Internet-Draft.
723         """
724         d = defer.Deferred()
725         d.addCallback(self._cbTestPacketSTATUSShort)
726         self.ftc.openRequests[1] = d
727         data = struct.pack('!LL', 1, filetransfer.FX_OK)
728         self.ftc.packet_STATUS(data)
729         return d
730
731
732     def _cbTestPacketSTATUSShort(self, result):
733         """
734         Assert that the result is a two-tuple containing empty strings, since
735         the STATUS packet had neither a message nor a language.
736         """
737         self.assertEqual(result[0], '')
738         self.assertEqual(result[1], '')
739
740
741     def test_packetSTATUSWithoutLang(self):
742         """
743         A STATUS packet containing a result code and a message but no language
744         can also be parsed to produce the result of an outstanding request
745         L{Deferred}.  Such packets are sent by some SFTP implementations, though
746         not strictly legal.
747
748         @see: U{section 9.1<http://tools.ietf.org/html/draft-ietf-secsh-filexfer-13#section-9.1>}
749             of the SFTP Internet-Draft.
750         """
751         d = defer.Deferred()
752         d.addCallback(self._cbTestPacketSTATUSWithoutLang)
753         self.ftc.openRequests[1] = d
754         data = struct.pack('!LL', 1, filetransfer.FX_OK) + common.NS('msg')
755         self.ftc.packet_STATUS(data)
756         return d
757
758
759     def _cbTestPacketSTATUSWithoutLang(self, result):
760         """
761         Assert that the result is a two-tuple containing the message from the
762         STATUS packet and an empty string, since the language was missing.
763         """
764         self.assertEqual(result[0], 'msg')
765         self.assertEqual(result[1], '')