]> code.delx.au - pymsnt/blob - src/tlib/msn/test_msn.py
File transfer test works! We can send files to MSN Messenger 7. Jabber glue not done...
[pymsnt] / src / tlib / msn / test_msn.py
1 # Copyright (c) 2001-2005 Twisted Matrix Laboratories.
2 # Copyright (c) 2005 James Bunton
3 # See LICENSE for details.
4
5 """
6 Test cases for msn.
7 """
8
9 # Twisted imports
10 from twisted.protocols import loopback
11 from twisted.protocols.basic import LineReceiver
12 from twisted.internet.defer import Deferred
13 from twisted.internet import reactor, main
14 from twisted.python import failure
15 from twisted.trial import unittest
16
17 # System imports
18 import StringIO, sys, urllib, random, struct
19
20 import msn
21 import msnft
22
23
24 def printError(f):
25 print f
26
27 class StringIOWithoutClosing(StringIO.StringIO):
28 disconnecting = 0
29 def close(self): pass
30 def loseConnection(self): pass
31
32 class LoopbackCon:
33 def __init__(self, con1, con2):
34 self.con1 = con1
35 self.con2 = con2
36 self.con1ToCon2 = loopback.LoopbackRelay(con1)
37 self.con2ToCon1 = loopback.LoopbackRelay(con2)
38 con2.makeConnection(self.con1ToCon2)
39 con1.makeConnection(self.con2ToCon1)
40 self.connected = True
41
42 def doSteps(self, steps=1):
43 """ Returns true if the connection finished """
44 count = 0
45 while count < steps:
46 reactor.iterate(0.01)
47 self.con1ToCon2.clearBuffer()
48 self.con2ToCon1.clearBuffer()
49 if self.con1ToCon2.shouldLose:
50 self.con1ToCon2.clearBuffer()
51 count = -1
52 break
53 elif self.con2ToCon1.shouldLose:
54 count = -1
55 break
56 else:
57 count += 1
58 if count == -1:
59 self.disconnect()
60 return True
61 return False
62
63 def disconnect(self):
64 if self.connected:
65 self.con1.connectionLost(failure.Failure(main.CONNECTION_DONE))
66 self.con2.connectionLost(failure.Failure(main.CONNECTION_DONE))
67 reactor.iterate()
68
69
70
71
72 ##################
73 # Passport tests #
74 ##################
75
76 class PassportTests(unittest.TestCase):
77
78 def setUp(self):
79 self.result = []
80 self.deferred = Deferred()
81 self.deferred.addCallback(lambda r: self.result.append(r))
82 self.deferred.addErrback(printError)
83
84 def testNexus(self):
85 protocol = msn.PassportNexus(self.deferred, 'https://foobar.com/somepage.quux')
86 headers = {
87 'Content-Length' : '0',
88 'Content-Type' : 'text/html',
89 'PassportURLs' : 'DARealm=Passport.Net,DALogin=login.myserver.com/,DAReg=reg.myserver.com'
90 }
91 transport = StringIOWithoutClosing()
92 protocol.makeConnection(transport)
93 protocol.dataReceived('HTTP/1.0 200 OK\r\n')
94 for (h,v) in headers.items(): protocol.dataReceived('%s: %s\r\n' % (h,v))
95 protocol.dataReceived('\r\n')
96 self.failUnless(self.result[0] == "https://login.myserver.com/")
97
98 def _doLoginTest(self, response, headers):
99 protocol = msn.PassportLogin(self.deferred,'foo@foo.com','testpass','https://foo.com/', 'a')
100 protocol.makeConnection(StringIOWithoutClosing())
101 protocol.dataReceived(response)
102 for (h,v) in headers.items(): protocol.dataReceived('%s: %s\r\n' % (h,v))
103 protocol.dataReceived('\r\n')
104
105 def testPassportLoginSuccess(self):
106 headers = {
107 'Content-Length' : '0',
108 'Content-Type' : 'text/html',
109 'Authentication-Info' : "Passport1.4 da-status=success,tname=MSPAuth," +
110 "tname=MSPProf,tname=MSPSec,from-PP='somekey'," +
111 "ru=http://messenger.msn.com"
112 }
113 self._doLoginTest('HTTP/1.1 200 OK\r\n', headers)
114 self.failUnless(self.result[0] == (msn.LOGIN_SUCCESS, 'somekey'))
115
116 def testPassportLoginFailure(self):
117 headers = {
118 'Content-Type' : 'text/html',
119 'WWW-Authenticate' : 'Passport1.4 da-status=failed,' +
120 'srealm=Passport.NET,ts=-3,prompt,cburl=http://host.com,' +
121 'cbtxt=the%20error%20message'
122 }
123 self._doLoginTest('HTTP/1.1 401 Unauthorized\r\n', headers)
124 self.failUnless(self.result[0] == (msn.LOGIN_FAILURE, 'the error message'))
125
126 def testPassportLoginRedirect(self):
127 headers = {
128 'Content-Type' : 'text/html',
129 'Authentication-Info' : 'Passport1.4 da-status=redir',
130 'Location' : 'https://newlogin.host.com/'
131 }
132 self._doLoginTest('HTTP/1.1 302 Found\r\n', headers)
133 self.failUnless(self.result[0] == (msn.LOGIN_REDIRECT, 'https://newlogin.host.com/', 'a'))
134
135
136
137 ######################
138 # Notification tests #
139 ######################
140
141 class DummyNotificationClient(msn.NotificationClient):
142 def loggedIn(self, userHandle, verified):
143 if userHandle == 'foo@bar.com' and verified:
144 self.state = 'LOGIN'
145
146 def gotProfile(self, message):
147 self.state = 'PROFILE'
148
149 def gotContactStatus(self, userHandle, code, screenName):
150 if code == msn.STATUS_AWAY and userHandle == "foo@bar.com" and screenName == "Test Screen Name":
151 c = self.factory.contacts.getContact(userHandle)
152 if c.caps & msn.MSNContact.MSNC1 and c.msnobj:
153 self.state = 'INITSTATUS'
154
155 def contactStatusChanged(self, userHandle, code, screenName):
156 if code == msn.STATUS_LUNCH and userHandle == "foo@bar.com" and screenName == "Test Name":
157 self.state = 'NEWSTATUS'
158
159 def contactAvatarChanged(self, userHandle, hash):
160 if userHandle == "foo@bar.com" and hash == "trC8SlFx2sWQxZMIBAWSEnXc8oQ=":
161 self.state = 'NEWAVATAR'
162 elif self.state == 'NEWAVATAR' and hash == "":
163 self.state = 'AVATARGONE'
164
165 def contactPersonalChanged(self, userHandle, personal):
166 if userHandle == 'foo@bar.com' and personal == 'My Personal Message':
167 self.state = 'GOTPERSONAL'
168 elif userHandle == 'foo@bar.com' and personal == '':
169 self.state = 'PERSONALGONE'
170
171 def contactOffline(self, userHandle):
172 if userHandle == "foo@bar.com": self.state = 'OFFLINE'
173
174 def statusChanged(self, code):
175 if code == msn.STATUS_HIDDEN: self.state = 'MYSTATUS'
176
177 def listSynchronized(self, *args):
178 self.state = 'GOTLIST'
179
180 def gotPhoneNumber(self, userHandle, phoneType, number):
181 self.state = 'GOTPHONE'
182
183 def userRemovedMe(self, userHandle):
184 c = self.factory.contacts.getContact(userHandle)
185 if not c: self.state = 'USERREMOVEDME'
186
187 def userAddedMe(self, userGuid, userHandle, screenName):
188 c = self.factory.contacts.getContact(userHandle)
189 if c and (c.lists | msn.PENDING_LIST) and (screenName == 'Screen Name'):
190 self.state = 'USERADDEDME'
191
192 def gotSwitchboardInvitation(self, sessionID, host, port, key, userHandle, screenName):
193 if sessionID == 1234 and \
194 host == '192.168.1.1' and \
195 port == 1863 and \
196 key == '123.456' and \
197 userHandle == 'foo@foo.com' and \
198 screenName == 'Screen Name':
199 self.state = 'SBINVITED'
200
201 def gotMSNAlert(self, body, action, subscr):
202 self.state = 'NOTIFICATION'
203
204 def gotInitialEmailNotification(self, inboxunread, foldersunread):
205 if inboxunread == 1 and foldersunread == 0:
206 self.state = 'INITEMAIL1'
207 else:
208 self.state = 'INITEMAIL2'
209
210 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
211 if mailfrom == 'Some Person' and fromaddr == 'example@passport.com' and subject == 'newsubject':
212 self.state = 'REALTIMEEMAIL'
213
214 class NotificationTests(unittest.TestCase):
215 """ testing the various events in NotificationClient """
216
217 def setUp(self):
218 self.client = DummyNotificationClient()
219 self.client.factory = msn.NotificationFactory()
220 self.client.state = 'START'
221
222 def tearDown(self):
223 self.client = None
224
225 def testLogin(self):
226 self.client.lineReceived('USR 1 OK foo@bar.com 1')
227 self.failUnless((self.client.state == 'LOGIN'), 'Failed to detect successful login')
228
229 def testProfile(self):
230 m = 'MSG Hotmail Hotmail 353\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsprofile; charset=UTF-8\r\n'
231 m += 'LoginTime: 1016941010\r\nEmailEnabled: 1\r\nMemberIdHigh: 40000\r\nMemberIdLow: -600000000\r\nlang_preference: 1033\r\n'
232 m += 'preferredEmail: foo@bar.com\r\ncountry: AU\r\nPostalCode: 90210\r\nGender: M\r\nKid: 0\r\nAge:\r\nsid: 400\r\n'
233 m += 'kv: 2\r\nMSPAuth: 2CACCBCCADMoV8ORoz64BVwmjtksIg!kmR!Rj5tBBqEaW9hc4YnPHSOQ$$\r\n\r\n'
234 self.client.dataReceived(m)
235 self.failUnless((self.client.state == 'PROFILE'), 'Failed to detect initial profile')
236
237 def testInitialEmailNotification(self):
238 m = 'MIME-Version: 1.0\r\nContent-Type: text/x-msmsgsinitialemailnotification; charset=UTF-8\r\n'
239 m += '\r\nInbox-Unread: 1\r\nFolders-Unread: 0\r\nInbox-URL: /cgi-bin/HoTMaiL\r\n'
240 m += 'Folders-URL: /cgi-bin/folders\r\nPost-URL: http://www.hotmail.com\r\n\r\n'
241 m = 'MSG Hotmail Hotmail %s\r\n' % (str(len(m))) + m
242 self.client.dataReceived(m)
243 self.failUnless((self.client.state == 'INITEMAIL1'), 'Failed to detect initial email notification')
244
245 def testNoInitialEmailNotification(self):
246 m = 'MIME-Version: 1.0\r\nContent-Type: text/x-msmsgsinitialemailnotification; charset=UTF-8\r\n'
247 m += '\r\nInbox-Unread: 0\r\nFolders-Unread: 0\r\nInbox-URL: /cgi-bin/HoTMaiL\r\n'
248 m += 'Folders-URL: /cgi-bin/folders\r\nPost-URL: http://www.hotmail.com\r\n\r\n'
249 m = 'MSG Hotmail Hotmail %s\r\n' % (str(len(m))) + m
250 self.client.dataReceived(m)
251 self.failUnless((self.client.state != 'INITEMAIL2'), 'Detected initial email notification when I should not have')
252
253 def testRealtimeEmailNotification(self):
254 m = 'MSG Hotmail Hotmail 356\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsemailnotification; charset=UTF-8\r\n'
255 m += '\r\nFrom: Some Person\r\nMessage-URL: /cgi-bin/getmsg?msg=MSG1050451140.21&start=2310&len=2059&curmbox=ACTIVE\r\n'
256 m += 'Post-URL: https://loginnet.passport.com/ppsecure/md5auth.srf?lc=1038\r\n'
257 m += 'Subject: =?"us-ascii"?Q?newsubject?=\r\nDest-Folder: ACTIVE\r\nFrom-Addr: example@passport.com\r\nid: 2\r\n'
258 self.client.dataReceived(m)
259 self.failUnless((self.client.state == 'REALTIMEEMAIL'), 'Failed to detect realtime email notification')
260
261 def testMSNAlert(self):
262 m = '<NOTIFICATION ver="2" id="1342902633" siteid="199999999" siteurl="http://alerts.msn.com">\r\n'
263 m += '<TO pid="0x0006BFFD:0x8582C0FB" name="example@passport.com"/>\r\n'
264 m += '<MSG pri="1" id="1342902633">\r\n'
265 m += '<SUBSCR url="http://g.msn.com/3ALMSNTRACKING/199999999ToastChange?http://alerts.msn.com/Alerts/MyAlerts.aspx?strela=1"/>\r\n'
266 m += '<ACTION url="http://g.msn.com/3ALMSNTRACKING/199999999ToastAction?http://alerts.msn.com/Alerts/MyAlerts.aspx?strela=1"/>\r\n'
267 m += '<BODY lang="3076" icon="">\r\n'
268 m += '<TEXT>utf8-encoded text</TEXT></BODY></MSG>\r\n'
269 m += '</NOTIFICATION>\r\n'
270 cmd = 'NOT %s\r\n' % str(len(m))
271 m = cmd + m
272 # Whee, lots of fun to test that lineReceived & dataReceived work well with input coming
273 # in in (fairly) arbitrary chunks.
274 map(self.client.dataReceived, [x+'\r\n' for x in m.split('\r\n')[:-1]])
275 self.failUnless((self.client.state == 'NOTIFICATION'), 'Failed to detect MSN Alert message')
276
277 def testListSync(self):
278 self.client.makeConnection(StringIOWithoutClosing())
279 msn.NotificationClient.loggedIn(self.client, 'foo@foo.com', 1)
280 lines = [
281 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 3" % self.client.currentID,
282 "GTC A",
283 "BLP AL",
284 "LSG Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
285 "LSG Other%20Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz",
286 "LSG More%20Other%20Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyya",
287 "LST N=userHandle@email.com F=Some%20Name C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 13 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy,yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz",
288 ]
289 map(self.client.lineReceived, lines)
290 contacts = self.client.factory.contacts
291 contact = contacts.getContact('userHandle@email.com')
292 #self.failUnless(contacts.version == 100, "Invalid contact list version")
293 self.failUnless(contact.screenName == 'Some Name', "Invalid screen-name for user")
294 self.failUnless(contacts.groups == {'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy': 'Friends', \
295 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz': 'Other Friends', \
296 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyya': 'More Other Friends'} \
297 , "Did not get proper group list")
298 self.failUnless(contact.groups == ['yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy', \
299 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz'] and \
300 contact.lists == 13, "Invalid contact list/group info")
301 self.failUnless(self.client.state == 'GOTLIST', "Failed to call list sync handler")
302 self.client.logOut()
303
304 def testStatus(self):
305 # Set up the contact list
306 self.client.makeConnection(StringIOWithoutClosing())
307 msn.NotificationClient.loggedIn(self.client, 'foo@foo.com', 1)
308 lines = [
309 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self.client.currentID,
310 "GTC A",
311 "BLP AL",
312 "LST N=foo@bar.com F=Some%20Name C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 13 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy,yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz",
313 ]
314 map(self.client.lineReceived, lines)
315 # Now test!
316 msnobj = urllib.quote('<msnobj Creator="buddy1@hotmail.com" Size="24539" Type="3" Location="TFR2C.tmp" Friendly="AAA=" SHA1D="trC8SlFx2sWQxZMIBAWSEnXc8oQ=" SHA1C="U32o6bosZzluJq82eAtMpx5dIEI="/>')
317 t = [('ILN 1 AWY foo@bar.com Test%20Screen%20Name 268435456 ' + msnobj, 'INITSTATUS', 'Failed to detect initial status report'),
318 ('NLN LUN foo@bar.com Test%20Name 0', 'NEWSTATUS', 'Failed to detect contact status change'),
319 ('NLN AWY foo@bar.com Test%20Name 0 ' + msnobj, 'NEWAVATAR', 'Failed to detect contact avatar change'),
320 ('NLN AWY foo@bar.com Test%20Name 0', 'AVATARGONE', 'Failed to detect contact avatar disappearing'),
321 ('FLN foo@bar.com', 'OFFLINE', 'Failed to detect contact signing off'),
322 ('CHG 1 HDN 0 ' + msnobj, 'MYSTATUS', 'Failed to detect my status changing')]
323 for i in t:
324 self.client.lineReceived(i[0])
325 self.failUnless((self.client.state == i[1]), i[2])
326
327 # Test UBX
328 self.client.dataReceived('UBX foo@bar.com 72\r\n<Data><PSM>My Personal Message</PSM><CurrentMedia></CurrentMedia></Data>')
329 self.failUnless((self.client.state == 'GOTPERSONAL'), 'Failed to detect new personal message')
330 self.client.dataReceived('UBX foo@bar.com 0\r\n')
331 self.failUnless((self.client.state == 'PERSONALGONE'), 'Failed to detect personal message disappearing')
332 self.client.logOut()
333
334 def testAsyncPhoneChange(self):
335 c = msn.MSNContact(userHandle='userHandle@email.com')
336 self.client.factory.contacts = msn.MSNContactList()
337 self.client.factory.contacts.addContact(c)
338 self.client.makeConnection(StringIOWithoutClosing())
339 self.client.lineReceived("BPR 101 userHandle@email.com PHH 123%20456")
340 c = self.client.factory.contacts.getContact('userHandle@email.com')
341 self.failUnless(self.client.state == 'GOTPHONE', "Did not fire phone change callback")
342 self.failUnless(c.homePhone == '123 456', "Did not update the contact's phone number")
343 self.failUnless(self.client.factory.contacts.version == 101, "Did not update list version")
344
345 def testLateBPR(self):
346 """
347 This test makes sure that if a BPR response that was meant
348 to be part of a SYN response (but came after the last LST)
349 is received, the correct contact is updated and all is well
350 """
351 self.client.makeConnection(StringIOWithoutClosing())
352 msn.NotificationClient.loggedIn(self.client, 'foo@foo.com', 1)
353 lines = [
354 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self.client.currentID,
355 "GTC A",
356 "BLP AL",
357 "LSG Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
358 "LST N=userHandle@email.com F=Some%20Name C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 13 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
359 "BPR PHH 123%20456"
360 ]
361 map(self.client.lineReceived, lines)
362 contact = self.client.factory.contacts.getContact('userHandle@email.com')
363 self.failUnless(contact.homePhone == '123 456', "Did not update contact's phone number")
364 self.client.logOut()
365
366 def testUserRemovedMe(self):
367 self.client.factory.contacts = msn.MSNContactList()
368 contact = msn.MSNContact(userHandle='foo@foo.com')
369 contact.addToList(msn.REVERSE_LIST)
370 self.client.factory.contacts.addContact(contact)
371 self.client.lineReceived("REM 0 RL foo@foo.com")
372 self.failUnless(self.client.state == 'USERREMOVEDME', "Failed to remove user from reverse list")
373
374 def testUserAddedMe(self):
375 self.client.factory.contacts = msn.MSNContactList()
376 self.client.lineReceived("ADC 0 RL N=foo@foo.com F=Screen%20Name")
377 self.failUnless(self.client.state == 'USERADDEDME', "Failed to add user to reverse lise")
378
379 def testAsyncSwitchboardInvitation(self):
380 self.client.lineReceived("RNG 1234 192.168.1.1:1863 CKI 123.456 foo@foo.com Screen%20Name")
381 self.failUnless((self.client.state == 'SBINVITED'), 'Failed to detect switchboard invitation')
382
383
384 #######################################
385 # Notification with fake server tests #
386 #######################################
387
388 class FakeNotificationServer(msn.MSNEventBase):
389 def handle_CHG(self, params):
390 if len(params) < 4:
391 params.append('')
392 self.sendLine("CHG %s %s %s %s" % (params[0], params[1], params[2], params[3]))
393
394 def handle_BLP(self, params):
395 self.sendLine("BLP %s %s 100" % (params[0], params[1]))
396
397 def handle_ADC(self, params):
398 trid = params[0]
399 list = msn.listCodeToID[params[1].lower()]
400 if list == msn.FORWARD_LIST:
401 userHandle = ""
402 screenName = ""
403 userGuid = ""
404 groups = ""
405 for p in params[2:]:
406 if p[0] == 'N':
407 userHandle = p[2:]
408 elif p[0] == 'F':
409 screenName = p[2:]
410 elif p[0] == 'C':
411 userGuid = p[2:]
412 else:
413 groups = p
414 if userHandle and userGuid:
415 self.transport.loseConnection()
416 return
417 if userHandle:
418 if not screenName:
419 self.transport.loseConnection()
420 else:
421 self.sendLine("ADC %s FL N=%s F=%s C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx %s" % (trid, userHandle, screenName, groups))
422 return
423 if userGuid:
424 raise "NotImplementedError"
425 else:
426 if len(params) != 3:
427 self.transport.loseConnection()
428 if not params[2].startswith("N=") and params[2].count('@') == 1:
429 self.transport.loseConnection()
430 self.sendLine("ADC %s %s %s" % (params[0], params[1], params[2]))
431
432 def handle_REM(self, params):
433 if len(params) != 3:
434 self.transport.loseConnection()
435 return
436 try:
437 trid = int(params[0])
438 listType = msn.listCodeToID[params[1].lower()]
439 except:
440 self.transport.loseConnection()
441 if listType == msn.FORWARD_LIST and params[2].count('@') > 0:
442 self.transport.loseConnection()
443 elif listType != msn.FORWARD_LIST and params[2].count('@') != 1:
444 self.transport.loseConnection()
445 else:
446 self.sendLine("REM %s %s %s" % (params[0], params[1], params[2]))
447
448 def handle_PRP(self, params):
449 if len(params) != 3:
450 self.transport.loseConnection()
451 if params[1] == "MFN":
452 self.sendLine("PRP %s MFN %s" % (params[0], params[2]))
453 else:
454 # Only friendly names are implemented
455 self.transport.loseConnection()
456
457 def handle_UUX(self, params):
458 if len(params) != 2:
459 self.transport.loseConnection()
460 return
461 l = int(params[1])
462 if l > 0:
463 self.currentMessage = msn.MSNMessage(length=l, userHandle=params[0], screenName="UUX", specialMessage=True)
464 self.setRawMode()
465 else:
466 self.sendLine("UUX %s 0" % params[0])
467
468 def checkMessage(self, message):
469 if message.specialMessage:
470 if message.screenName == "UUX":
471 self.sendLine("UUX %s 0" % message.userHandle)
472 return 0
473 return 1
474
475 def handle_XFR(self, params):
476 if len(params) != 2:
477 self.transport.loseConnection()
478 return
479 if params[1] != "SB":
480 self.transport.loseConnection()
481 return
482 self.sendLine("XFR %s SB 129.129.129.129:1234 CKI SomeSecret" % params[0])
483
484
485
486 class FakeNotificationClient(msn.NotificationClient):
487 def doStatusChange(self):
488 def testcb((status,)):
489 if status == msn.STATUS_AWAY:
490 self.test = 'PASS'
491 self.transport.loseConnection()
492 d = self.changeStatus(msn.STATUS_AWAY)
493 d.addCallback(testcb)
494
495 def doPrivacyMode(self):
496 def testcb((priv,)):
497 if priv.upper() == 'AL':
498 self.test = 'PASS'
499 self.transport.loseConnection()
500 d = self.setPrivacyMode(True)
501 d.addCallback(testcb)
502
503 def doAddContactFL(self):
504 def testcb((listType, userGuid, userHandle, screenName)):
505 if listType & msn.FORWARD_LIST and \
506 userGuid == "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" and \
507 userHandle == "foo@bar.com" and \
508 screenName == "foo@bar.com" and \
509 self.factory.contacts.getContact(userHandle):
510 self.test = 'PASS'
511 self.transport.loseConnection()
512 d = self.addContact(msn.FORWARD_LIST, "foo@bar.com")
513 d.addCallback(testcb)
514
515 def doAddContactAL(self):
516 def testcb((listType, userGuid, userHandle, screenName)):
517 if listType & msn.ALLOW_LIST and \
518 userHandle == "foo@bar.com" and \
519 not userGuid and not screenName and \
520 self.factory.contacts.getContact(userHandle):
521 self.test = 'PASS'
522 self.transport.loseConnection()
523 d = self.addContact(msn.ALLOW_LIST, "foo@bar.com")
524 d.addCallback(testcb)
525
526 def doRemContactFL(self):
527 def testcb((listType, userHandle, groupID)):
528 if listType & msn.FORWARD_LIST and \
529 userHandle == "foo@bar.com":
530 self.test = 'PASS'
531 self.transport.loseConnection()
532 d = self.remContact(msn.FORWARD_LIST, "foo@bar.com")
533 d.addCallback(testcb)
534
535 def doRemContactAL(self):
536 def testcb((listType, userHandle, groupID)):
537 if listType & msn.ALLOW_LIST and \
538 userHandle == "foo@bar.com":
539 self.test = 'PASS'
540 self.transport.loseConnection()
541 d = self.remContact(msn.ALLOW_LIST, "foo@bar.com")
542 d.addCallback(testcb)
543
544 def doScreenNameChange(self):
545 def testcb((screenName,)):
546 if screenName == "Some new name":
547 self.test = 'PASS'
548 self.transport.loseConnection()
549 d = self.changeScreenName("Some new name")
550 d.addCallback(testcb)
551
552 def doPersonalChange(self, personal):
553 def testcb((checkPersonal,)):
554 if checkPersonal == personal:
555 self.test = 'PASS'
556 self.transport.loseConnection()
557 d = self.changePersonalMessage(personal)
558 d.addCallback(testcb)
559
560 def doAvatarChange(self, data):
561 def testcb(ignored):
562 self.test = 'PASS'
563 self.transport.loseConnection()
564 d = self.changeAvatar(data, True)
565 d.addCallback(testcb)
566
567 def doRequestSwitchboard(self):
568 def testcb((host, port, key)):
569 if host == "129.129.129.129" and port == 1234 and key == "SomeSecret":
570 self.test = 'PASS'
571 self.transport.loseConnection()
572 d = self.requestSwitchboardServer()
573 d.addCallback(testcb)
574
575 class FakeServerNotificationTests(unittest.TestCase):
576 """ tests the NotificationClient against a fake server. """
577
578 def setUp(self):
579 self.client = FakeNotificationClient()
580 self.client.factory = msn.NotificationFactory()
581 self.client.test = 'FAIL'
582 self.server = FakeNotificationServer()
583 self.loop = LoopbackCon(self.client, self.server)
584
585 def tearDown(self):
586 self.loop.disconnect()
587
588 def testChangeStatus(self):
589 self.client.doStatusChange()
590 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
591 self.failUnless((self.client.test == 'PASS'), 'Failed to change status properly')
592
593 def testSetPrivacyMode(self):
594 self.client.factory.contacts = msn.MSNContactList()
595 self.client.doPrivacyMode()
596 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
597 self.failUnless((self.client.test == 'PASS'), 'Failed to change privacy mode')
598
599 def testSyncList(self):
600 self.client.doSyncList()
601 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
602 self.failUnless((self.client.test == 'PASS'), 'Failed to synchronise list')
603 testSyncList.skip = "Will do after list versions."
604
605 def testAddContactFL(self):
606 self.client.factory.contacts = msn.MSNContactList()
607 self.client.doAddContactFL()
608 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
609 self.failUnless((self.client.test == 'PASS'), 'Failed to add contact to forward list')
610
611 def testAddContactAL(self):
612 self.client.factory.contacts = msn.MSNContactList()
613 self.client.doAddContactAL()
614 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
615 self.failUnless((self.client.test == 'PASS'), 'Failed to add contact to allow list')
616
617 def testRemContactFL(self):
618 self.client.factory.contacts = msn.MSNContactList()
619 self.client.factory.contacts.addContact(msn.MSNContact(userGuid="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", userHandle="foo@bar.com", screenName="Some guy", lists=msn.FORWARD_LIST))
620 self.client.doRemContactFL()
621 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
622 self.failUnless((self.client.test == 'PASS'), 'Failed to remove contact from forward list')
623
624 def testRemContactAL(self):
625 self.client.factory.contacts = msn.MSNContactList()
626 self.client.factory.contacts.addContact(msn.MSNContact(userGuid="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", userHandle="foo@bar.com", screenName="Some guy", lists=msn.ALLOW_LIST))
627 self.client.doRemContactAL()
628 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
629 self.failUnless((self.client.test == 'PASS'), 'Failed to remove contact from allow list')
630
631 def testChangedScreenName(self):
632 self.client.doScreenNameChange()
633 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
634 self.failUnless((self.client.test == 'PASS'), 'Failed to change screen name properly')
635
636 def testChangePersonal1(self):
637 self.client.doPersonalChange("Some personal message")
638 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
639 self.failUnless((self.client.test == 'PASS'), 'Failed to change personal message properly')
640
641 def testChangePersonal2(self):
642 self.client.doPersonalChange("")
643 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
644 self.failUnless((self.client.test == 'PASS'), 'Failed to change personal message properly')
645
646 def testChangeAvatar(self):
647 self.client.doAvatarChange("DATADATADATADATA")
648 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
649 self.failUnless((self.client.test == 'PASS'), 'Failed to change avatar properly')
650
651 def testRequestSwitchboard(self):
652 self.client.doRequestSwitchboard()
653 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
654 self.failUnless((self.client.test == 'PASS'), 'Failed to request switchboard')
655
656
657 #################################
658 # Notification challenges tests #
659 #################################
660
661 class DummyChallengeNotificationServer(msn.MSNEventBase):
662 def doChallenge(self, challenge, response):
663 self.state = 0
664 self.response = response
665 self.sendLine("CHL 0 " + challenge)
666
667 def checkMessage(self, message):
668 if message.message == self.response:
669 self.state = "PASS"
670 self.transport.loseConnection()
671 return 0
672
673 def handle_QRY(self, params):
674 self.state = 1
675 if len(params) == 3 and params[1] == "PROD0090YUAUV{2B" and params[2] == "32":
676 self.state = 2
677 self.currentMessage = msn.MSNMessage(length=32, userHandle="QRY", screenName="QRY", specialMessage=True)
678 self.setRawMode()
679 else:
680 self.transport.loseConnection()
681
682 class DummyChallengeNotificationClient(msn.NotificationClient):
683 def connectionMade(self):
684 msn.MSNEventBase.connectionMade(self)
685
686 def handle_CHL(self, params):
687 msn.NotificationClient.handle_CHL(self, params)
688 self.transport.loseConnection()
689
690
691 class NotificationChallengeTests(unittest.TestCase):
692 """ tests the responses to the CHLs the server sends """
693
694 def setUp(self):
695 self.client = DummyChallengeNotificationClient()
696 self.server = DummyChallengeNotificationServer()
697 self.loop = LoopbackCon(self.client, self.server)
698
699 def tearDown(self):
700 self.loop.disconnect()
701
702 def testChallenges(self):
703 challenges = [('13038318816579321232', 'b01c13020e374d4fa20abfad6981b7a9'),
704 ('23055170411503520698', 'ae906c3f2946d25e7da1b08b0b247659'),
705 ('37819769320541083311', 'db79d37dadd9031bef996893321da480'),
706 ('93662730714769834295', 'd619dfbb1414004d34d0628766636568'),
707 ('31154116582196216093', '95e96c4f8cfdba6f065c8869b5e984e9')]
708 for challenge, response in challenges:
709 self.server.doChallenge(challenge, response)
710 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
711 self.failUnless((self.server.state == 'PASS'), 'Incorrect challenge response.')
712
713
714 ###########################
715 # Notification ping tests #
716 ###########################
717
718 class DummyPingNotificationServer(LineReceiver):
719 def lineReceived(self, line):
720 if line.startswith("PNG") and self.good:
721 self.sendLine("QNG 50")
722
723 class DummyPingNotificationClient(msn.NotificationClient):
724 def connectionMade(self):
725 self.pingCheckerStart()
726
727 def sendLine(self, line):
728 msn.NotificationClient.sendLine(self, line)
729 self.count += 1
730 if self.count > 10:
731 self.transport.loseConnection() # But not for real, just to end the test
732
733 def connectionLost(self, reason):
734 if self.count <= 10:
735 self.state = 'DISCONNECTED'
736
737 class NotificationPingTests(unittest.TestCase):
738 """ tests pinging in the NotificationClient class """
739
740 def setUp(self):
741 msn.PINGSPEED = 0.1
742 self.client = DummyPingNotificationClient()
743 self.server = DummyPingNotificationServer()
744 self.client.state = 'CONNECTED'
745 self.client.count = 0
746 self.loop = LoopbackCon(self.client, self.server)
747
748 def tearDown(self):
749 msn.PINGSPEED = 50.0
750 self.client.logOut()
751 self.loop.disconnect()
752
753 def testPingGood(self):
754 self.server.good = True
755 self.loop.doSteps(100)
756 self.failUnless((self.client.state == 'CONNECTED'), 'Should be connected.')
757
758 def testPingBad(self):
759 self.server.good = False
760 self.loop.doSteps(100)
761 self.failUnless((self.client.state == 'DISCONNECTED'), 'Should be disconnected.')
762
763
764
765
766 ###########################
767 # Switchboard basic tests #
768 ###########################
769
770 class DummySwitchboardServer(msn.MSNEventBase):
771 def handle_USR(self, params):
772 if len(params) != 3:
773 self.transport.loseConnection()
774 if params[1] == 'foo@bar.com' and params[2] == 'somekey':
775 self.sendLine("USR %s OK %s %s" % (params[0], params[1], params[1]))
776
777 def handle_ANS(self, params):
778 if len(params) != 4:
779 self.transport.loseConnection()
780 if params[1] == 'foo@bar.com' and params[2] == 'somekey' and params[3] == 'someSID':
781 self.sendLine("ANS %s OK" % params[0])
782
783 def handle_CAL(self, params):
784 if len(params) != 2:
785 self.transport.loseConnection()
786 if params[1] == 'friend@hotmail.com':
787 self.sendLine("CAL %s RINGING 1111122" % params[0])
788 else:
789 self.transport.loseConnection()
790
791 def checkMessage(self, message):
792 if message.message == 'Hi how are you today?':
793 self.sendLine("ACK " + message.userHandle) # Relies on TRID getting stored in userHandle trick
794 else:
795 self.transport.loseConnection()
796 return 0
797
798 class DummySwitchboardClient(msn.SwitchboardClient):
799 def loggedIn(self):
800 self.state = 'LOGGEDIN'
801 self.transport.loseConnection()
802
803 def gotChattingUsers(self, users):
804 if users == {'fred@hotmail.com': 'fred', 'jack@email.com': 'jack has a nickname!'}:
805 self.state = 'GOTCHATTINGUSERS'
806
807 def userJoined(self, userHandle, screenName):
808 if userHandle == "friend@hotmail.com" and screenName == "friend nickname":
809 self.state = 'USERJOINED'
810
811 def userLeft(self, userHandle):
812 if userHandle == "friend@hotmail.com":
813 self.state = 'USERLEFT'
814
815 def gotContactTyping(self, message):
816 if message.userHandle == 'foo@bar.com':
817 self.state = 'USERTYPING'
818
819 def gotMessage(self, message):
820 if message.userHandle == 'friend@hotmail.com' and \
821 message.screenName == 'Friend Nickname' and \
822 message.message == 'Hello.':
823 self.state = 'GOTMESSAGE'
824
825 def doSendInvite(self):
826 def testcb((sid,)):
827 if sid == 1111122:
828 self.state = 'INVITESUCCESS'
829 self.transport.loseConnection()
830 d = self.inviteUser('friend@hotmail.com')
831 d.addCallback(testcb)
832
833 def doSendMessage(self):
834 def testcb(ignored):
835 self.state = 'MESSAGESUCCESS'
836 self.transport.loseConnection()
837 m = msn.MSNMessage()
838 m.setHeader("Content-Type", "text/plain; charset=UTF-8")
839 m.message = 'Hi how are you today?'
840 m.ack = msn.MSNMessage.MESSAGE_ACK
841 d = self.sendMessage(m)
842 d.addCallback(testcb)
843
844
845 class SwitchboardBasicTests(unittest.TestCase):
846 """ Tests basic functionality of switchboard sessions """
847 def setUp(self):
848 self.client = DummySwitchboardClient()
849 self.client.state = 'START'
850 self.client.userHandle = 'foo@bar.com'
851 self.client.key = 'somekey'
852 self.client.sessionID = 'someSID'
853 self.server = DummySwitchboardServer()
854 self.loop = LoopbackCon(self.client, self.server)
855
856 def tearDown(self):
857 self.loop.disconnect()
858
859 def _testSB(self, reply):
860 self.client.reply = reply
861 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
862 self.failUnless((self.client.state == 'LOGGEDIN'), 'Failed to login with reply='+str(reply))
863
864 def testReply(self):
865 self._testSB(True)
866
867 def testAsync(self):
868 self._testSB(False)
869
870 def testChattingUsers(self):
871 lines = ["IRO 1 1 2 fred@hotmail.com fred",
872 "IRO 1 2 2 jack@email.com jack%20has%20a%20nickname%21"]
873 for line in lines:
874 self.client.lineReceived(line)
875 self.failUnless((self.client.state == 'GOTCHATTINGUSERS'), 'Failed to get chatting users')
876
877 def testUserJoined(self):
878 self.client.lineReceived("JOI friend@hotmail.com friend%20nickname")
879 self.failUnless((self.client.state == 'USERJOINED'), 'Failed to notice user joining')
880
881 def testUserLeft(self):
882 self.client.lineReceived("BYE friend@hotmail.com")
883 self.failUnless((self.client.state == 'USERLEFT'), 'Failed to notice user leaving')
884
885 def testTypingCheck(self):
886 m = 'MSG foo@bar.com Foo 80\r\n'
887 m += 'MIME-Version: 1.0\r\n'
888 m += 'Content-Type: text/x-msmsgscontrol\r\n'
889 m += 'TypingUser: foo@bar\r\n'
890 m += '\r\n\r\n'
891 self.client.dataReceived(m)
892 self.failUnless((self.client.state == 'USERTYPING'), 'Failed to detect typing notification')
893
894 def testGotMessage(self):
895 m = 'MSG friend@hotmail.com Friend%20Nickname 68\r\n'
896 m += 'MIME-Version: 1.0\r\n'
897 m += 'Content-Type: text/plain; charset=UTF-8\r\n'
898 m += '\r\nHello.'
899 self.client.dataReceived(m)
900 self.failUnless((self.client.state == 'GOTMESSAGE'), 'Failed to detect message')
901
902 def testInviteUser(self):
903 self.client.connectionMade = lambda: None
904 self.client.doSendInvite()
905 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
906 self.failUnless((self.client.state == 'INVITESUCCESS'), 'Failed to invite user')
907
908 def testSendMessage(self):
909 self.client.connectionMade = lambda: None
910 self.client.doSendMessage()
911 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
912 self.failUnless((self.client.state == 'MESSAGESUCCESS'), 'Failed to send message')
913
914
915 ################
916 # MSNP2P tests #
917 ################
918
919 class DummySwitchboardP2PServerHelper(msn.MSNEventBase):
920 def __init__(self, server):
921 msn.MSNEventBase.__init__(self)
922 self.server = server
923
924 def handle_USR(self, params):
925 if len(params) != 3:
926 self.transport.loseConnection()
927 self.userHandle = params[1]
928 if params[1] == 'foo1@bar.com' and params[2] == 'somekey1':
929 self.sendLine("USR %s OK %s %s" % (params[0], params[1], params[1]))
930 if params[1] == 'foo2@bar.com' and params[2] == 'somekey2':
931 self.sendLine("USR %s OK %s %s" % (params[0], params[1], params[1]))
932
933 def checkMessage(self, message):
934 return 1
935
936 def gotMessage(self, message):
937 message.userHandle = self.userHandle
938 message.screenName = self.userHandle
939 self.server.gotMessage(message, self)
940
941 def sendMessage(self, message):
942 if message.length == 0: message.length = message._calcMessageLen()
943 self.sendLine("MSG %s %s %s" % (message.userHandle, message.screenName, message.length))
944 self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
945 self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
946 for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
947 self.sendLine("%s: %s" % (header[0], header[1]))
948 self.transport.write("\r\n")
949 self.transport.write(message.message)
950
951
952 class DummySwitchboardP2PServer:
953 def __init__(self):
954 self.clients = []
955
956 def newClient(self):
957 c = DummySwitchboardP2PServerHelper(self)
958 self.clients.append(c)
959 return c
960
961 def gotMessage(self, message, sender):
962 for c in self.clients:
963 if c != sender:
964 c.sendMessage(message)
965
966 class DummySwitchboardP2PClient(msn.SwitchboardClient):
967 def gotMessage(self, message):
968 if message.message == "Test Message" and message.userHandle == "foo1@bar.com":
969 self.status = "GOTMESSAGE"
970
971 def gotFileReceive(self, fileReceive):
972 self.fileReceive = fileReceive
973
974 class SwitchboardP2PTests(unittest.TestCase):
975 def setUp(self):
976 self.server = DummySwitchboardP2PServer()
977 self.client1 = DummySwitchboardP2PClient()
978 self.client1.key = 'somekey1'
979 self.client1.userHandle = 'foo1@bar.com'
980 self.client2 = DummySwitchboardP2PClient()
981 self.client2.key = 'somekey2'
982 self.client2.userHandle = 'foo2@bar.com'
983 self.client2.status = "INIT"
984 self.loop1 = LoopbackCon(self.client1, self.server.newClient())
985 self.loop2 = LoopbackCon(self.client2, self.server.newClient())
986
987 def tearDown(self):
988 self.loop1.disconnect()
989 self.loop2.disconnect()
990
991 def _loop(self, steps=1):
992 for i in xrange(steps):
993 self.loop1.doSteps(1)
994 self.loop2.doSteps(1)
995
996 def testMessage(self):
997 self.client1.sendMessage(msn.MSNMessage(message='Test Message'))
998 self._loop()
999 self.failUnless((self.client2.status == "GOTMESSAGE"), "Fake switchboard server not working.")
1000
1001 def _generateData(self):
1002 data = ''
1003 for i in xrange(3000):
1004 data += struct.pack("<L", random.randint(0, sys.maxint))
1005 return data
1006
1007 def testAvatars(self):
1008 self.gotAvatar = False
1009
1010 # Set up the avatar for client1
1011 imageData = self._generateData()
1012 self.client1.msnobj = msn.MSNObject()
1013 self.client1.msnobj.setData('foo1@bar.com', imageData)
1014 self.client1.msnobj.makeText()
1015
1016 # Make client2 request the avatar
1017 def avatarCallback((data,)):
1018 self.gotAvatar = (data == imageData)
1019 msnContact = msn.MSNContact(userHandle='foo1@bar.com', msnobj=self.client1.msnobj)
1020 d = self.client2.sendAvatarRequest(msnContact)
1021 d.addCallback(avatarCallback)
1022
1023 # Let them do their thing
1024 self._loop(10)
1025
1026 # Check that client2 got the avatar
1027 self.failUnless((self.gotAvatar), "Failed to transfer avatar")
1028
1029 def testFilesHappyPath(self):
1030 fileData = self._generateData()
1031 self.gotFile = False
1032
1033 # Send the file (client2->client1)
1034 msnContact = msn.MSNContact(userHandle='foo1@bar.com', caps=msn.MSNContact.MSNC1)
1035 fileSend, d = self.client2.sendFile(msnContact, "myfile.txt", len(fileData))
1036 def accepted((yes,)):
1037 if yes:
1038 fileSend.write(fileData)
1039 fileSend.close()
1040 else:
1041 raise "TransferDeclined"
1042 def failed():
1043 raise "TransferError"
1044 d.addCallback(accepted)
1045 d.addErrback(failed)
1046
1047 # Let the request get pushed to client1
1048 self._loop(10)
1049
1050 # Receive the file
1051 def finished(data):
1052 self.gotFile = (data == fileData)
1053 fileBuffer = msn.StringBuffer(finished)
1054 fileReceive = self.client1.fileReceive
1055 self.failUnless((fileReceive.filename == "myfile.txt" and fileReceive.filesize == len(fileData)), "Filename or length wrong.")
1056 fileReceive.accept(fileBuffer)
1057
1058 # Lets transfer!!
1059 self._loop(10)
1060
1061 self.failUnless((self.gotFile), "Failed to transfer file")
1062
1063 def testFilesDeclinePath(self):
1064 fileData = self._generateData()
1065 self.gotDecline = False
1066
1067 # Send the file (client2->client1)
1068 msnContact = msn.MSNContact(userHandle='foo1@bar.com', caps=msn.MSNContact.MSNC1)
1069 fileSend, d = self.client2.sendFile(msnContact, "myfile.txt", len(fileData))
1070 def accepted((yes,)):
1071 self.failUnless((not yes), "Failed to understand a decline.")
1072 self.gotDecline = True
1073 def failed():
1074 raise "TransferError"
1075 d.addCallback(accepted)
1076 d.addErrback(failed)
1077
1078 # Let the request get pushed to client1
1079 self._loop(10)
1080
1081 # Decline the file
1082 fileReceive = self.client1.fileReceive
1083 fileReceive.reject()
1084
1085 # Let the decline get pushed to client2
1086 self._loop(10)
1087
1088 self.failUnless((self.gotDecline), "Failed to understand a decline, ignored.")
1089
1090
1091 ################
1092 # MSNFTP tests #
1093 ################
1094
1095 class FileTransferTestCase(unittest.TestCase):
1096 """ test FileSend against FileReceive """
1097
1098 def setUp(self):
1099 self.input = StringIOWithoutClosing()
1100 self.input.writelines(['a'] * 7000)
1101 self.input.seek(0)
1102 self.output = StringIOWithoutClosing()
1103
1104 def tearDown(self):
1105 self.input = None
1106 self.output = None
1107
1108 def testFileTransfer(self):
1109 auth = 1234
1110 sender = msnft.MSNFTP_FileSend(self.input)
1111 sender.auth = auth
1112 sender.fileSize = 7000
1113 client = msnft.MSNFTP_FileReceive(auth, "foo@bar.com", self.output)
1114 client.fileSize = 7000
1115 loop = LoopbackCon(client, sender)
1116 loop.doSteps(100)
1117 self.failUnless((client.completed and sender.completed), "send failed to complete")
1118 self.failUnless((self.input.getvalue() == self.output.getvalue()), "saved file does not match original")
1119
1120