1 # Copyright (c) 2001-2005 Twisted Matrix Laboratories.
2 # Copyright (c) 2005-2006 James Bunton <james@delx.cjb.net>
3 # Licensed for distribution under the GPL version 2, check COPYING for details
17 from twisted
.protocols
import loopback
18 from twisted
.protocols
.basic
import LineReceiver
19 from twisted
.internet
.defer
import Deferred
20 from twisted
.internet
import reactor
, main
21 from twisted
.python
import failure
, log
22 from twisted
.trial
import unittest
25 import StringIO
, sys
, urllib
, random
, struct
30 log
.startLogging(sys
.stdout
)
36 class StringIOWithoutClosing(StringIO
.StringIO
):
39 def loseConnection(self
): pass
42 def __init__(self
, con1
, con2
):
48 self
.con1ToCon2
= loopback
.LoopbackRelay(self
.con1
)
49 self
.con2ToCon1
= loopback
.LoopbackRelay(self
.con2
)
50 self
.con2
.makeConnection(self
.con1ToCon2
)
51 self
.con1
.makeConnection(self
.con2ToCon1
)
54 def doSteps(self
, steps
=1):
55 """ Returns true if the connection finished """
59 self
.con1ToCon2
.clearBuffer()
60 self
.con2ToCon1
.clearBuffer()
61 if self
.con1ToCon2
.shouldLose
:
62 self
.con1ToCon2
.clearBuffer()
65 elif self
.con2ToCon1
.shouldLose
:
77 self
.con1
.connectionLost(failure
.Failure(main
.CONNECTION_DONE
))
78 self
.con2
.connectionLost(failure
.Failure(main
.CONNECTION_DONE
))
88 class PassportTests(unittest
.TestCase
):
92 self
.deferred
= Deferred()
93 self
.deferred
.addCallback(lambda r
: self
.result
.append(r
))
94 self
.deferred
.addErrback(printError
)
97 protocol
= msn
.PassportNexus(self
.deferred
, 'https://foobar.com/somepage.quux')
99 'Content-Length' : '0',
100 'Content-Type' : 'text/html',
101 'PassportURLs' : 'DARealm=Passport.Net,DALogin=login.myserver.com/,DAReg=reg.myserver.com'
103 transport
= StringIOWithoutClosing()
104 protocol
.makeConnection(transport
)
105 protocol
.dataReceived('HTTP/1.0 200 OK\r\n')
106 for (h
,v
) in headers
.items(): protocol
.dataReceived('%s: %s\r\n' % (h
,v
))
107 protocol
.dataReceived('\r\n')
108 self
.failUnless(self
.result
[0] == "https://login.myserver.com/")
110 def _doLoginTest(self
, response
, headers
):
111 protocol
= msn
.PassportLogin(self
.deferred
,'foo@foo.com','testpass','https://foo.com/', 'a')
112 protocol
.makeConnection(StringIOWithoutClosing())
113 protocol
.dataReceived(response
)
114 for (h
,v
) in headers
.items(): protocol
.dataReceived('%s: %s\r\n' % (h
,v
))
115 protocol
.dataReceived('\r\n')
117 def testPassportLoginSuccess(self
):
119 'Content-Length' : '0',
120 'Content-Type' : 'text/html',
121 'Authentication-Info' : "Passport1.4 da-status=success,tname=MSPAuth," +
122 "tname=MSPProf,tname=MSPSec,from-PP='somekey'," +
123 "ru=http://messenger.msn.com"
125 self
._doLoginTest
('HTTP/1.1 200 OK\r\n', headers
)
126 self
.failUnless(self
.result
[0] == (msn
.LOGIN_SUCCESS
, 'somekey'))
128 def testPassportLoginFailure(self
):
130 'Content-Type' : 'text/html',
131 'WWW-Authenticate' : 'Passport1.4 da-status=failed,' +
132 'srealm=Passport.NET,ts=-3,prompt,cburl=http://host.com,' +
133 'cbtxt=the%20error%20message'
135 self
._doLoginTest
('HTTP/1.1 401 Unauthorized\r\n', headers
)
136 self
.failUnless(self
.result
[0] == (msn
.LOGIN_FAILURE
, 'the error message'))
138 def testPassportLoginRedirect(self
):
140 'Content-Type' : 'text/html',
141 'Authentication-Info' : 'Passport1.4 da-status=redir',
142 'Location' : 'https://newlogin.host.com/'
144 self
._doLoginTest
('HTTP/1.1 302 Found\r\n', headers
)
145 self
.failUnless(self
.result
[0] == (msn
.LOGIN_REDIRECT
, 'https://newlogin.host.com/', 'a'))
149 ######################
150 # Notification tests #
151 ######################
153 class DummyNotificationClient(msn
.NotificationClient
):
154 def loggedIn(self
, userHandle
, verified
):
155 if userHandle
== 'foo@bar.com' and verified
:
158 def gotProfile(self
, message
):
159 self
.state
= 'PROFILE'
161 def gotContactStatus(self
, userHandle
, code
, screenName
):
162 if code
== msn
.STATUS_AWAY
and userHandle
== "foo@bar.com" and screenName
== "Test Screen Name":
163 c
= self
.factory
.contacts
.getContact(userHandle
)
164 if c
.caps
& msn
.MSNContact
.MSNC1
and c
.msnobj
:
165 self
.state
= 'INITSTATUS'
167 def contactStatusChanged(self
, userHandle
, code
, screenName
):
168 if code
== msn
.STATUS_LUNCH
and userHandle
== "foo@bar.com" and screenName
== "Test Name":
169 self
.state
= 'NEWSTATUS'
171 def contactAvatarChanged(self
, userHandle
, hash):
172 if userHandle
== "foo@bar.com" and hash == "b6b0bc4a5171dac590c593080405921275dcf284":
173 self
.state
= 'NEWAVATAR'
174 elif self
.state
== 'NEWAVATAR' and hash == "":
175 self
.state
= 'AVATARGONE'
177 def contactPersonalChanged(self
, userHandle
, personal
):
178 if userHandle
== 'foo@bar.com' and personal
== 'My Personal Message':
179 self
.state
= 'GOTPERSONAL'
180 elif userHandle
== 'foo@bar.com' and personal
== '':
181 self
.state
= 'PERSONALGONE'
183 def contactOffline(self
, userHandle
):
184 if userHandle
== "foo@bar.com": self
.state
= 'OFFLINE'
186 def statusChanged(self
, code
):
187 if code
== msn
.STATUS_HIDDEN
: self
.state
= 'MYSTATUS'
189 def listSynchronized(self
, *args
):
190 self
.state
= 'GOTLIST'
192 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
193 self
.state
= 'GOTPHONE'
195 def userRemovedMe(self
, userHandle
):
196 c
= self
.factory
.contacts
.getContact(userHandle
)
197 if not c
: self
.state
= 'USERREMOVEDME'
199 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
200 c
= self
.factory
.contacts
.getContact(userHandle
)
201 if c
and (c
.lists | msn
.PENDING_LIST
) and (screenName
== 'Screen Name'):
202 self
.state
= 'USERADDEDME'
204 def gotSwitchboardInvitation(self
, sessionID
, host
, port
, key
, userHandle
, screenName
):
205 if sessionID
== 1234 and \
206 host
== '192.168.1.1' and \
208 key
== '123.456' and \
209 userHandle
== 'foo@foo.com' and \
210 screenName
== 'Screen Name':
211 self
.state
= 'SBINVITED'
213 def gotMSNAlert(self
, body
, action
, subscr
):
214 self
.state
= 'NOTIFICATION'
216 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
217 if inboxunread
== 1 and foldersunread
== 0:
218 self
.state
= 'INITEMAIL1'
220 self
.state
= 'INITEMAIL2'
222 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
223 if mailfrom
== 'Some Person' and fromaddr
== 'example@passport.com' and subject
== 'newsubject':
224 self
.state
= 'REALTIMEEMAIL'
226 class NotificationTests(unittest
.TestCase
):
227 """ testing the various events in NotificationClient """
230 self
.client
= DummyNotificationClient()
231 self
.client
.factory
= msn
.NotificationFactory()
232 msn
.MSNEventBase
.connectionMade(self
.client
)
233 self
.client
.state
= 'START'
239 self
.client
.lineReceived('USR 1 OK foo@bar.com 1')
240 self
.failUnless((self
.client
.state
== 'LOGIN'), 'Failed to detect successful login')
242 def testProfile(self
):
243 m
= 'MSG Hotmail Hotmail 353\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsprofile; charset=UTF-8\r\n'
244 m
+= 'LoginTime: 1016941010\r\nEmailEnabled: 1\r\nMemberIdHigh: 40000\r\nMemberIdLow: -600000000\r\nlang_preference: 1033\r\n'
245 m
+= 'preferredEmail: foo@bar.com\r\ncountry: AU\r\nPostalCode: 90210\r\nGender: M\r\nKid: 0\r\nAge:\r\nsid: 400\r\n'
246 m
+= 'kv: 2\r\nMSPAuth: 2CACCBCCADMoV8ORoz64BVwmjtksIg!kmR!Rj5tBBqEaW9hc4YnPHSOQ$$\r\n\r\n'
247 self
.client
.dataReceived(m
)
248 self
.failUnless((self
.client
.state
== 'PROFILE'), 'Failed to detect initial profile')
250 def testInitialEmailNotification(self
):
251 m
= 'MIME-Version: 1.0\r\nContent-Type: text/x-msmsgsinitialemailnotification; charset=UTF-8\r\n'
252 m
+= '\r\nInbox-Unread: 1\r\nFolders-Unread: 0\r\nInbox-URL: /cgi-bin/HoTMaiL\r\n'
253 m
+= 'Folders-URL: /cgi-bin/folders\r\nPost-URL: http://www.hotmail.com\r\n\r\n'
254 m
= 'MSG Hotmail Hotmail %s\r\n' % (str(len(m
))) + m
255 self
.client
.dataReceived(m
)
256 self
.failUnless((self
.client
.state
== 'INITEMAIL1'), 'Failed to detect initial email notification')
258 def testNoInitialEmailNotification(self
):
259 m
= 'MIME-Version: 1.0\r\nContent-Type: text/x-msmsgsinitialemailnotification; charset=UTF-8\r\n'
260 m
+= '\r\nInbox-Unread: 0\r\nFolders-Unread: 0\r\nInbox-URL: /cgi-bin/HoTMaiL\r\n'
261 m
+= 'Folders-URL: /cgi-bin/folders\r\nPost-URL: http://www.hotmail.com\r\n\r\n'
262 m
= 'MSG Hotmail Hotmail %s\r\n' % (str(len(m
))) + m
263 self
.client
.dataReceived(m
)
264 self
.failUnless((self
.client
.state
!= 'INITEMAIL2'), 'Detected initial email notification when I should not have')
266 def testRealtimeEmailNotification(self
):
267 m
= 'MSG Hotmail Hotmail 356\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsemailnotification; charset=UTF-8\r\n'
268 m
+= '\r\nFrom: Some Person\r\nMessage-URL: /cgi-bin/getmsg?msg=MSG1050451140.21&start=2310&len=2059&curmbox=ACTIVE\r\n'
269 m
+= 'Post-URL: https://loginnet.passport.com/ppsecure/md5auth.srf?lc=1038\r\n'
270 m
+= 'Subject: =?"us-ascii"?Q?newsubject?=\r\nDest-Folder: ACTIVE\r\nFrom-Addr: example@passport.com\r\nid: 2\r\n'
271 self
.client
.dataReceived(m
)
272 self
.failUnless((self
.client
.state
== 'REALTIMEEMAIL'), 'Failed to detect realtime email notification')
274 def testMSNAlert(self
):
275 m
= '<NOTIFICATION ver="2" id="1342902633" siteid="199999999" siteurl="http://alerts.msn.com">\r\n'
276 m
+= '<TO pid="0x0006BFFD:0x8582C0FB" name="example@passport.com"/>\r\n'
277 m
+= '<MSG pri="1" id="1342902633">\r\n'
278 m
+= '<SUBSCR url="http://g.msn.com/3ALMSNTRACKING/199999999ToastChange?http://alerts.msn.com/Alerts/MyAlerts.aspx?strela=1"/>\r\n'
279 m
+= '<ACTION url="http://g.msn.com/3ALMSNTRACKING/199999999ToastAction?http://alerts.msn.com/Alerts/MyAlerts.aspx?strela=1"/>\r\n'
280 m
+= '<BODY lang="3076" icon="">\r\n'
281 m
+= '<TEXT>utf8-encoded text</TEXT></BODY></MSG>\r\n'
282 m
+= '</NOTIFICATION>\r\n'
283 cmd
= 'NOT %s\r\n' % str(len(m
))
285 # Whee, lots of fun to test that lineReceived & dataReceived work well with input coming
286 # in in (fairly) arbitrary chunks.
287 map(self
.client
.dataReceived
, [x
+'\r\n' for x
in m
.split('\r\n')[:-1]])
288 self
.failUnless((self
.client
.state
== 'NOTIFICATION'), 'Failed to detect MSN Alert message')
290 def testListSync(self
):
291 self
.client
.makeConnection(StringIOWithoutClosing())
292 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
294 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 3" % self
.client
.currentID
,
297 "LSG Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
298 "LSG Other%20Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz",
299 "LSG More%20Other%20Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyya",
300 "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",
302 map(self
.client
.lineReceived
, lines
)
303 contacts
= self
.client
.factory
.contacts
304 contact
= contacts
.getContact('userHandle@email.com')
305 #self.failUnless(contacts.version == 100, "Invalid contact list version")
306 self
.failUnless(contact
.screenName
== 'Some Name', "Invalid screen-name for user")
307 self
.failUnless(contacts
.groups
== {'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy': 'Friends', \
308 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz': 'Other Friends', \
309 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyya': 'More Other Friends'} \
310 , "Did not get proper group list")
311 self
.failUnless(contact
.groups
== ['yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy', \
312 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz'] and \
313 contact
.lists
== 13, "Invalid contact list/group info")
314 self
.failUnless(self
.client
.state
== 'GOTLIST', "Failed to call list sync handler")
317 def testStatus(self
):
318 # Set up the contact list
319 self
.client
.makeConnection(StringIOWithoutClosing())
320 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
322 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self
.client
.currentID
,
325 "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",
327 map(self
.client
.lineReceived
, lines
)
329 msnobj
= urllib
.quote('<msnobj Creator="buddy1@hotmail.com" Size="24539" Type="3" Location="TFR2C.tmp" Friendly="AAA=" SHA1D="trC8SlFx2sWQxZMIBAWSEnXc8oQ=" SHA1C="U32o6bosZzluJq82eAtMpx5dIEI="/>')
330 t
= [('ILN 1 AWY foo@bar.com Test%20Screen%20Name 268435456 ' + msnobj
, 'INITSTATUS', 'Failed to detect initial status report'),
331 ('NLN LUN foo@bar.com Test%20Name 0', 'NEWSTATUS', 'Failed to detect contact status change'),
332 ('NLN AWY foo@bar.com Test%20Name 0 ' + msnobj
, 'NEWAVATAR', 'Failed to detect contact avatar change'),
333 ('NLN AWY foo@bar.com Test%20Name 0', 'AVATARGONE', 'Failed to detect contact avatar disappearing'),
334 ('FLN foo@bar.com', 'OFFLINE', 'Failed to detect contact signing off'),
335 ('CHG 1 HDN 0 ' + msnobj
, 'MYSTATUS', 'Failed to detect my status changing')]
337 self
.client
.lineReceived(i
[0])
338 self
.failUnless((self
.client
.state
== i
[1]), i
[2])
341 self
.client
.dataReceived('UBX foo@bar.com 72\r\n<Data><PSM>My Personal Message</PSM><CurrentMedia></CurrentMedia></Data>')
342 self
.failUnless((self
.client
.state
== 'GOTPERSONAL'), 'Failed to detect new personal message')
343 self
.client
.dataReceived('UBX foo@bar.com 0\r\n')
344 self
.failUnless((self
.client
.state
== 'PERSONALGONE'), 'Failed to detect personal message disappearing')
347 def testAsyncPhoneChange(self
):
348 c
= msn
.MSNContact(userHandle
='userHandle@email.com')
349 self
.client
.factory
.contacts
= msn
.MSNContactList()
350 self
.client
.factory
.contacts
.addContact(c
)
351 self
.client
.makeConnection(StringIOWithoutClosing())
352 self
.client
.lineReceived("BPR 101 userHandle@email.com PHH 123%20456")
353 c
= self
.client
.factory
.contacts
.getContact('userHandle@email.com')
354 self
.failUnless(self
.client
.state
== 'GOTPHONE', "Did not fire phone change callback")
355 self
.failUnless(c
.homePhone
== '123 456', "Did not update the contact's phone number")
356 self
.failUnless(self
.client
.factory
.contacts
.version
== 101, "Did not update list version")
358 def testLateBPR(self
):
360 This test makes sure that if a BPR response that was meant
361 to be part of a SYN response (but came after the last LST)
362 is received, the correct contact is updated and all is well
364 self
.client
.makeConnection(StringIOWithoutClosing())
365 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
367 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self
.client
.currentID
,
370 "LSG Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
371 "LST N=userHandle@email.com F=Some%20Name C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 13 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
374 map(self
.client
.lineReceived
, lines
)
375 contact
= self
.client
.factory
.contacts
.getContact('userHandle@email.com')
376 self
.failUnless(contact
.homePhone
== '123 456', "Did not update contact's phone number")
379 def testUserRemovedMe(self
):
380 self
.client
.factory
.contacts
= msn
.MSNContactList()
381 contact
= msn
.MSNContact(userHandle
='foo@foo.com')
382 contact
.addToList(msn
.REVERSE_LIST
)
383 self
.client
.factory
.contacts
.addContact(contact
)
384 self
.client
.lineReceived("REM 0 RL foo@foo.com")
385 self
.failUnless(self
.client
.state
== 'USERREMOVEDME', "Failed to remove user from reverse list")
387 def testUserAddedMe(self
):
388 self
.client
.factory
.contacts
= msn
.MSNContactList()
389 self
.client
.lineReceived("ADC 0 RL N=foo@foo.com F=Screen%20Name")
390 self
.failUnless(self
.client
.state
== 'USERADDEDME', "Failed to add user to reverse lise")
392 def testAsyncSwitchboardInvitation(self
):
393 self
.client
.lineReceived("RNG 1234 192.168.1.1:1863 CKI 123.456 foo@foo.com Screen%20Name")
394 self
.failUnless((self
.client
.state
== 'SBINVITED'), 'Failed to detect switchboard invitation')
397 #######################################
398 # Notification with fake server tests #
399 #######################################
401 class FakeNotificationServer(msn
.MSNEventBase
):
402 def handle_CHG(self
, params
):
405 self
.sendLine("CHG %s %s %s %s" % (params
[0], params
[1], params
[2], params
[3]))
407 def handle_BLP(self
, params
):
408 self
.sendLine("BLP %s %s 100" % (params
[0], params
[1]))
410 def handle_ADC(self
, params
):
412 list = msn
.listCodeToID
[params
[1].lower()]
413 if list == msn
.FORWARD_LIST
:
427 if userHandle
and userGuid
:
428 self
.transport
.loseConnection()
432 self
.transport
.loseConnection()
434 self
.sendLine("ADC %s FL N=%s F=%s C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx %s" % (trid
, userHandle
, screenName
, groups
))
437 raise "NotImplementedError"
440 self
.transport
.loseConnection()
441 if not params
[2].startswith("N=") and params
[2].count('@') == 1:
442 self
.transport
.loseConnection()
443 self
.sendLine("ADC %s %s %s" % (params
[0], params
[1], params
[2]))
445 def handle_REM(self
, params
):
447 self
.transport
.loseConnection()
450 trid
= int(params
[0])
451 listType
= msn
.listCodeToID
[params
[1].lower()]
453 self
.transport
.loseConnection()
454 if listType
== msn
.FORWARD_LIST
and params
[2].count('@') > 0:
455 self
.transport
.loseConnection()
456 elif listType
!= msn
.FORWARD_LIST
and params
[2].count('@') != 1:
457 self
.transport
.loseConnection()
459 self
.sendLine("REM %s %s %s" % (params
[0], params
[1], params
[2]))
461 def handle_PRP(self
, params
):
463 self
.transport
.loseConnection()
464 if params
[1] == "MFN":
465 self
.sendLine("PRP %s MFN %s" % (params
[0], params
[2]))
467 # Only friendly names are implemented
468 self
.transport
.loseConnection()
470 def handle_UUX(self
, params
):
472 self
.transport
.loseConnection()
476 self
.currentMessage
= msn
.MSNMessage(length
=l
, userHandle
=params
[0], screenName
="UUX", specialMessage
=True)
479 self
.sendLine("UUX %s 0" % params
[0])
481 def checkMessage(self
, message
):
482 if message
.specialMessage
:
483 if message
.screenName
== "UUX":
484 self
.sendLine("UUX %s 0" % message
.userHandle
)
488 def handle_XFR(self
, params
):
490 self
.transport
.loseConnection()
492 if params
[1] != "SB":
493 self
.transport
.loseConnection()
495 self
.sendLine("XFR %s SB 129.129.129.129:1234 CKI SomeSecret" % params
[0])
499 class FakeNotificationClient(msn
.NotificationClient
):
500 def doStatusChange(self
):
501 def testcb((status
,)):
502 if status
== msn
.STATUS_AWAY
:
504 self
.transport
.loseConnection()
505 d
= self
.changeStatus(msn
.STATUS_AWAY
)
506 d
.addCallback(testcb
)
508 def doPrivacyMode(self
):
510 if priv
.upper() == 'AL':
512 self
.transport
.loseConnection()
513 d
= self
.setPrivacyMode(True)
514 d
.addCallback(testcb
)
516 def doAddContactFL(self
):
517 def testcb((listType
, userGuid
, userHandle
, screenName
)):
518 if listType
& msn
.FORWARD_LIST
and \
519 userGuid
== "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" and \
520 userHandle
== "foo@bar.com" and \
521 screenName
== "foo@bar.com" and \
522 self
.factory
.contacts
.getContact(userHandle
):
524 self
.transport
.loseConnection()
525 d
= self
.addContact(msn
.FORWARD_LIST
, "foo@bar.com")
526 d
.addCallback(testcb
)
528 def doAddContactAL(self
):
529 def testcb((listType
, userGuid
, userHandle
, screenName
)):
530 if listType
& msn
.ALLOW_LIST
and \
531 userHandle
== "foo@bar.com" and \
532 not userGuid
and not screenName
and \
533 self
.factory
.contacts
.getContact(userHandle
):
535 self
.transport
.loseConnection()
536 d
= self
.addContact(msn
.ALLOW_LIST
, "foo@bar.com")
537 d
.addCallback(testcb
)
539 def doRemContactFL(self
):
540 def testcb((listType
, userHandle
, groupID
)):
541 if listType
& msn
.FORWARD_LIST
and \
542 userHandle
== "foo@bar.com":
544 self
.transport
.loseConnection()
545 d
= self
.remContact(msn
.FORWARD_LIST
, "foo@bar.com")
546 d
.addCallback(testcb
)
548 def doRemContactAL(self
):
549 def testcb((listType
, userHandle
, groupID
)):
550 if listType
& msn
.ALLOW_LIST
and \
551 userHandle
== "foo@bar.com":
553 self
.transport
.loseConnection()
554 d
= self
.remContact(msn
.ALLOW_LIST
, "foo@bar.com")
555 d
.addCallback(testcb
)
557 def doScreenNameChange(self
):
560 self
.transport
.loseConnection()
561 d
= self
.changeScreenName("Some new name")
562 d
.addCallback(testcb
)
564 def doPersonalChange(self
, personal
):
565 def testcb((checkPersonal
,)):
566 if checkPersonal
== personal
:
568 self
.transport
.loseConnection()
569 d
= self
.changePersonalMessage(personal
)
570 d
.addCallback(testcb
)
572 def doAvatarChange(self
, dataFunc
):
575 self
.transport
.loseConnection()
576 d
= self
.changeAvatar(dataFunc
, True)
577 d
.addCallback(testcb
)
579 def doRequestSwitchboard(self
):
580 def testcb((host
, port
, key
)):
581 if host
== "129.129.129.129" and port
== 1234 and key
== "SomeSecret":
583 self
.transport
.loseConnection()
584 d
= self
.requestSwitchboardServer()
585 d
.addCallback(testcb
)
587 class FakeServerNotificationTests(unittest
.TestCase
):
588 """ tests the NotificationClient against a fake server. """
591 self
.client
= FakeNotificationClient()
592 self
.client
.factory
= msn
.NotificationFactory()
593 self
.client
.test
= 'FAIL'
594 self
.server
= FakeNotificationServer()
595 self
.loop
= LoopbackCon(self
.client
, self
.server
)
598 self
.loop
.disconnect()
600 def testChangeStatus(self
):
601 self
.client
.doStatusChange()
602 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
603 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change status properly')
605 def testSetPrivacyMode(self
):
606 self
.client
.factory
.contacts
= msn
.MSNContactList()
607 self
.client
.doPrivacyMode()
608 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
609 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change privacy mode')
611 def testAddContactFL(self
):
612 self
.client
.factory
.contacts
= msn
.MSNContactList()
613 self
.client
.doAddContactFL()
614 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
615 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to add contact to forward list')
617 def testAddContactAL(self
):
618 self
.client
.factory
.contacts
= msn
.MSNContactList()
619 self
.client
.doAddContactAL()
620 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
621 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to add contact to allow list')
623 def testRemContactFL(self
):
624 self
.client
.factory
.contacts
= msn
.MSNContactList()
625 self
.client
.factory
.contacts
.addContact(msn
.MSNContact(userGuid
="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", userHandle
="foo@bar.com", screenName
="Some guy", lists
=msn
.FORWARD_LIST
))
626 self
.client
.doRemContactFL()
627 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
628 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to remove contact from forward list')
630 def testRemContactAL(self
):
631 self
.client
.factory
.contacts
= msn
.MSNContactList()
632 self
.client
.factory
.contacts
.addContact(msn
.MSNContact(userGuid
="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", userHandle
="foo@bar.com", screenName
="Some guy", lists
=msn
.ALLOW_LIST
))
633 self
.client
.doRemContactAL()
634 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
635 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to remove contact from allow list')
637 def testChangedScreenName(self
):
638 self
.client
.doScreenNameChange()
639 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
640 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change screen name properly')
642 def testChangePersonal1(self
):
643 self
.client
.doPersonalChange("Some personal message")
644 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
645 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change personal message properly')
647 def testChangePersonal2(self
):
648 self
.client
.doPersonalChange("")
649 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
650 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change personal message properly')
652 def testChangeAvatar(self
):
653 self
.client
.doAvatarChange(lambda: "DATADATADATADATA")
654 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
655 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change avatar properly')
657 def testRequestSwitchboard(self
):
658 self
.client
.doRequestSwitchboard()
659 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
660 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to request switchboard')
663 #################################
664 # Notification challenges tests #
665 #################################
667 class DummyChallengeNotificationServer(msn
.MSNEventBase
):
668 def doChallenge(self
, challenge
, response
):
670 self
.response
= response
671 self
.sendLine("CHL 0 " + challenge
)
673 def checkMessage(self
, message
):
674 if message
.message
== self
.response
:
676 self
.transport
.loseConnection()
679 def handle_QRY(self
, params
):
681 if len(params
) == 3 and params
[1] == "PROD0090YUAUV{2B" and params
[2] == "32":
683 self
.currentMessage
= msn
.MSNMessage(length
=32, userHandle
="QRY", screenName
="QRY", specialMessage
=True)
686 self
.transport
.loseConnection()
688 class DummyChallengeNotificationClient(msn
.NotificationClient
):
689 def connectionMade(self
):
690 msn
.MSNEventBase
.connectionMade(self
)
692 def handle_CHL(self
, params
):
693 msn
.NotificationClient
.handle_CHL(self
, params
)
694 self
.transport
.loseConnection()
697 class NotificationChallengeTests(unittest
.TestCase
):
698 """ tests the responses to the CHLs the server sends """
701 self
.client
= DummyChallengeNotificationClient()
702 self
.server
= DummyChallengeNotificationServer()
703 self
.loop
= LoopbackCon(self
.client
, self
.server
)
706 self
.loop
.disconnect()
708 def testChallenges(self
):
709 challenges
= [('13038318816579321232', 'b01c13020e374d4fa20abfad6981b7a9'),
710 ('23055170411503520698', 'ae906c3f2946d25e7da1b08b0b247659'),
711 ('37819769320541083311', 'db79d37dadd9031bef996893321da480'),
712 ('93662730714769834295', 'd619dfbb1414004d34d0628766636568'),
713 ('31154116582196216093', '95e96c4f8cfdba6f065c8869b5e984e9')]
714 for challenge
, response
in challenges
:
715 self
.loop
.reconnect()
716 self
.server
.doChallenge(challenge
, response
)
717 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
718 self
.failUnless((self
.server
.state
== 'PASS'), 'Incorrect challenge response.')
721 ###########################
722 # Notification ping tests #
723 ###########################
725 class DummyPingNotificationServer(LineReceiver
):
726 def lineReceived(self
, line
):
727 if line
.startswith("PNG") and self
.good
:
728 self
.sendLine("QNG 50")
730 class DummyPingNotificationClient(msn
.NotificationClient
):
731 def connectionMade(self
):
732 msn
.MSNEventBase
.connectionMade(self
)
733 self
.pingCheckerStart()
735 def sendLine(self
, line
):
736 msn
.NotificationClient
.sendLine(self
, line
)
739 self
.transport
.loseConnection() # But not for real, just to end the test
741 def connectionLost(self
, reason
):
743 self
.state
= 'DISCONNECTED'
745 class NotificationPingTests(unittest
.TestCase
):
746 """ tests pinging in the NotificationClient class """
750 self
.client
= DummyPingNotificationClient()
751 self
.server
= DummyPingNotificationServer()
752 self
.client
.factory
= msn
.NotificationFactory()
753 self
.server
.factory
= msn
.NotificationFactory()
754 self
.client
.state
= 'CONNECTED'
755 self
.client
.count
= 0
756 self
.loop
= LoopbackCon(self
.client
, self
.server
)
761 self
.loop
.disconnect()
763 def testPingGood(self
):
764 self
.server
.good
= True
765 self
.loop
.doSteps(100)
766 self
.failUnless((self
.client
.state
== 'CONNECTED'), 'Should be connected.')
768 def testPingBad(self
):
769 self
.server
.good
= False
770 self
.loop
.doSteps(100)
771 self
.failUnless((self
.client
.state
== 'DISCONNECTED'), 'Should be disconnected.')
776 ###########################
777 # Switchboard basic tests #
778 ###########################
780 class DummySwitchboardServer(msn
.MSNEventBase
):
781 def handle_USR(self
, params
):
783 self
.transport
.loseConnection()
784 if params
[1] == 'foo@bar.com' and params
[2] == 'somekey':
785 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
787 def handle_ANS(self
, params
):
789 self
.transport
.loseConnection()
790 if params
[1] == 'foo@bar.com' and params
[2] == 'somekey' and params
[3] == 'someSID':
791 self
.sendLine("ANS %s OK" % params
[0])
793 def handle_CAL(self
, params
):
795 self
.transport
.loseConnection()
796 if params
[1] == 'friend@hotmail.com':
797 self
.sendLine("CAL %s RINGING 1111122" % params
[0])
799 self
.transport
.loseConnection()
801 def checkMessage(self
, message
):
802 if message
.message
== 'Hi how are you today?':
803 self
.sendLine("ACK " + message
.userHandle
) # Relies on TRID getting stored in userHandle trick
805 self
.transport
.loseConnection()
808 class DummySwitchboardClient(msn
.SwitchboardClient
):
810 self
.state
= 'LOGGEDIN'
811 self
.transport
.loseConnection()
813 def gotChattingUsers(self
, users
):
814 if users
== {'fred@hotmail.com': 'fred', 'jack@email.com': 'jack has a nickname!'}:
815 self
.state
= 'GOTCHATTINGUSERS'
817 def userJoined(self
, userHandle
, screenName
):
818 if userHandle
== "friend@hotmail.com" and screenName
== "friend nickname":
819 self
.state
= 'USERJOINED'
821 def userLeft(self
, userHandle
):
822 if userHandle
== "friend@hotmail.com":
823 self
.state
= 'USERLEFT'
825 def gotContactTyping(self
, message
):
826 if message
.userHandle
== 'foo@bar.com':
827 self
.state
= 'USERTYPING'
829 def gotMessage(self
, message
):
830 if message
.userHandle
== 'friend@hotmail.com' and \
831 message
.screenName
== 'Friend Nickname' and \
832 message
.message
== 'Hello.':
833 self
.state
= 'GOTMESSAGE'
835 def doSendInvite(self
):
838 self
.state
= 'INVITESUCCESS'
839 self
.transport
.loseConnection()
840 d
= self
.inviteUser('friend@hotmail.com')
841 d
.addCallback(testcb
)
843 def doSendMessage(self
):
845 self
.state
= 'MESSAGESUCCESS'
846 self
.transport
.loseConnection()
848 m
.setHeader("Content-Type", "text/plain; charset=UTF-8")
849 m
.message
= 'Hi how are you today?'
850 m
.ack
= msn
.MSNMessage
.MESSAGE_ACK
851 d
= self
.sendMessage(m
)
852 d
.addCallback(testcb
)
855 class SwitchboardBasicTests(unittest
.TestCase
):
856 """ Tests basic functionality of switchboard sessions """
858 self
.client
= DummySwitchboardClient()
859 self
.client
.state
= 'START'
860 self
.client
.userHandle
= 'foo@bar.com'
861 self
.client
.key
= 'somekey'
862 self
.client
.sessionID
= 'someSID'
863 self
.server
= DummySwitchboardServer()
864 self
.loop
= LoopbackCon(self
.client
, self
.server
)
867 self
.loop
.disconnect()
869 def _testSB(self
, reply
):
870 self
.client
.reply
= reply
871 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
872 self
.failUnless((self
.client
.state
== 'LOGGEDIN'), 'Failed to login with reply='+str(reply
))
880 def testChattingUsers(self
):
881 lines
= ["IRO 1 1 2 fred@hotmail.com fred",
882 "IRO 1 2 2 jack@email.com jack%20has%20a%20nickname%21"]
884 self
.client
.lineReceived(line
)
885 self
.failUnless((self
.client
.state
== 'GOTCHATTINGUSERS'), 'Failed to get chatting users')
887 def testUserJoined(self
):
888 self
.client
.lineReceived("JOI friend@hotmail.com friend%20nickname")
889 self
.failUnless((self
.client
.state
== 'USERJOINED'), 'Failed to notice user joining')
891 def testUserLeft(self
):
892 self
.client
.lineReceived("BYE friend@hotmail.com")
893 self
.failUnless((self
.client
.state
== 'USERLEFT'), 'Failed to notice user leaving')
895 def testTypingCheck(self
):
896 m
= 'MSG foo@bar.com Foo 80\r\n'
897 m
+= 'MIME-Version: 1.0\r\n'
898 m
+= 'Content-Type: text/x-msmsgscontrol\r\n'
899 m
+= 'TypingUser: foo@bar\r\n'
901 self
.client
.dataReceived(m
)
902 self
.failUnless((self
.client
.state
== 'USERTYPING'), 'Failed to detect typing notification')
904 def testGotMessage(self
):
905 m
= 'MSG friend@hotmail.com Friend%20Nickname 68\r\n'
906 m
+= 'MIME-Version: 1.0\r\n'
907 m
+= 'Content-Type: text/plain; charset=UTF-8\r\n'
909 self
.client
.dataReceived(m
)
910 self
.failUnless((self
.client
.state
== 'GOTMESSAGE'), 'Failed to detect message')
912 def testInviteUser(self
):
913 self
.client
.doSendInvite()
914 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
915 self
.failUnless((self
.client
.state
== 'INVITESUCCESS'), 'Failed to invite user')
917 def testSendMessage(self
):
918 self
.client
.doSendMessage()
919 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
920 self
.failUnless((self
.client
.state
== 'MESSAGESUCCESS'), 'Failed to send message')
927 class DummySwitchboardP2PServerHelper(msn
.MSNEventBase
):
928 def __init__(self
, server
):
929 msn
.MSNEventBase
.__init
__(self
)
932 def handle_USR(self
, params
):
934 self
.transport
.loseConnection()
935 self
.userHandle
= params
[1]
936 if params
[1] == 'foo1@bar.com' and params
[2] == 'somekey1':
937 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
938 if params
[1] == 'foo2@bar.com' and params
[2] == 'somekey2':
939 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
941 def checkMessage(self
, message
):
944 def gotMessage(self
, message
):
945 message
.userHandle
= self
.userHandle
946 message
.screenName
= self
.userHandle
947 self
.server
.gotMessage(message
, self
)
949 def sendMessage(self
, message
):
950 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
951 self
.sendLine("MSG %s %s %s" % (message
.userHandle
, message
.screenName
, message
.length
))
952 self
.sendLine('MIME-Version: %s' % message
.getHeader('MIME-Version'))
953 self
.sendLine('Content-Type: %s' % message
.getHeader('Content-Type'))
954 for header
in [h
for h
in message
.headers
.items() if h
[0].lower() not in ('mime-version','content-type')]:
955 self
.sendLine("%s: %s" % (header
[0], header
[1]))
956 self
.transport
.write("\r\n")
957 self
.transport
.write(message
.message
)
960 class DummySwitchboardP2PServer
:
965 c
= DummySwitchboardP2PServerHelper(self
)
966 self
.clients
.append(c
)
969 def gotMessage(self
, message
, sender
):
970 for c
in self
.clients
:
972 c
.sendMessage(message
)
974 class DummySwitchboardP2PClient(msn
.SwitchboardClient
):
975 def gotMessage(self
, message
):
976 if message
.message
== "Test Message" and message
.userHandle
== "foo1@bar.com":
977 self
.status
= "GOTMESSAGE"
979 def gotFileReceive(self
, fileReceive
):
980 self
.fileReceive
= fileReceive
982 class SwitchboardP2PTests(unittest
.TestCase
):
984 self
.server
= DummySwitchboardP2PServer()
985 self
.client1
= DummySwitchboardP2PClient()
986 self
.client1
.key
= 'somekey1'
987 self
.client1
.userHandle
= 'foo1@bar.com'
988 self
.client2
= DummySwitchboardP2PClient()
989 self
.client2
.key
= 'somekey2'
990 self
.client2
.userHandle
= 'foo2@bar.com'
991 self
.client2
.status
= "INIT"
992 self
.loop1
= LoopbackCon(self
.client1
, self
.server
.newClient())
993 self
.loop2
= LoopbackCon(self
.client2
, self
.server
.newClient())
996 self
.loop1
.disconnect()
997 self
.loop2
.disconnect()
999 def _loop(self
, steps
=1):
1000 for i
in xrange(steps
):
1001 self
.loop1
.doSteps(1)
1002 self
.loop2
.doSteps(1)
1004 def testMessage(self
):
1005 self
.client1
.sendMessage(msn
.MSNMessage(message
='Test Message'))
1007 self
.failUnless((self
.client2
.status
== "GOTMESSAGE"), "Fake switchboard server not working.")
1009 def _generateData(self
):
1011 for i
in xrange(3000):
1012 data
+= struct
.pack("<L", random
.randint(0, msn
.MSN_MAXINT
))
1015 def testAvatars(self
):
1016 self
.gotAvatar
= False
1018 # Set up the avatar for client1
1019 imageData
= self
._generateData
()
1020 self
.client1
.msnobj
= msn
.MSNObject()
1021 self
.client1
.msnobj
.setData('foo1@bar.com', lambda: imageData
)
1022 self
.client1
.msnobj
.makeText()
1024 # Make client2 request the avatar
1025 def avatarCallback((data
,)):
1026 self
.gotAvatar
= (data
== imageData
)
1027 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', msnobj
=self
.client1
.msnobj
)
1028 d
= self
.client2
.sendAvatarRequest(msnContact
)
1029 d
.addCallback(avatarCallback
)
1031 # Let them do their thing
1034 # Check that client2 got the avatar
1035 self
.failUnless((self
.gotAvatar
), "Failed to transfer avatar")
1037 def testFilesHappyPath(self
):
1038 fileData
= self
._generateData
()
1039 self
.gotFile
= False
1041 # Send the file (client2->client1)
1042 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1043 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1044 def accepted((yes
,)):
1046 fileSend
.write(fileData
)
1049 raise "TransferDeclined"
1051 raise "TransferError"
1052 d
.addCallback(accepted
)
1053 d
.addErrback(failed
)
1055 # Let the request get pushed to client1
1060 self
.gotFile
= (data
== fileData
)
1061 fileBuffer
= msn
.StringBuffer(finished
)
1062 fileReceive
= self
.client1
.fileReceive
1063 self
.failUnless((fileReceive
.filename
== "myfile.txt" and fileReceive
.filesize
== len(fileData
)), "Filename or length wrong.")
1064 fileReceive
.accept(fileBuffer
)
1069 self
.failUnless((self
.gotFile
), "Failed to transfer file")
1071 def testFilesHappyChunkedPath(self
):
1072 fileData
= self
._generateData
()
1073 self
.gotFile
= False
1075 # Send the file (client2->client1)
1076 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1077 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1078 def accepted((yes
,)):
1080 fileSend
.write(fileData
[:len(fileData
)/2])
1081 fileSend
.write(fileData
[len(fileData
)/2:])
1084 raise "TransferDeclined"
1086 raise "TransferError"
1087 d
.addCallback(accepted
)
1088 d
.addErrback(failed
)
1090 # Let the request get pushed to client1
1095 self
.gotFile
= (data
== fileData
)
1096 fileBuffer
= msn
.StringBuffer(finished
)
1097 fileReceive
= self
.client1
.fileReceive
1098 self
.failUnless((fileReceive
.filename
== "myfile.txt" and fileReceive
.filesize
== len(fileData
)), "Filename or length wrong.")
1099 fileReceive
.accept(fileBuffer
)
1104 self
.failUnless((self
.gotFile
), "Failed to transfer file")
1106 def testTwoFilesSequential(self
):
1107 self
.testFilesHappyPath()
1108 self
.testFilesHappyPath()
1110 def testFilesDeclinePath(self
):
1111 fileData
= self
._generateData
()
1112 self
.gotDecline
= False
1114 # Send the file (client2->client1)
1115 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1116 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1117 def accepted((yes
,)):
1118 self
.failUnless((not yes
), "Failed to understand a decline.")
1119 self
.gotDecline
= True
1121 raise "TransferError"
1122 d
.addCallback(accepted
)
1123 d
.addErrback(failed
)
1125 # Let the request get pushed to client1
1129 fileReceive
= self
.client1
.fileReceive
1130 fileReceive
.reject()
1132 # Let the decline get pushed to client2
1135 self
.failUnless((self
.gotDecline
), "Failed to understand a decline, ignored.")
1142 #class FileTransferTestCase(unittest.TestCase):
1143 # """ test FileSend against FileReceive """
1144 # skip = "Not implemented"
1147 # self.input = StringIOWithoutClosing()
1148 # self.input.writelines(['a'] * 7000)
1149 # self.input.seek(0)
1150 # self.output = StringIOWithoutClosing()
1152 # def tearDown(self):
1154 # self.output = None
1156 # def testFileTransfer(self):
1158 # sender = msnft.MSNFTP_FileSend(self.input)
1159 # sender.auth = auth
1160 # sender.fileSize = 7000
1161 # client = msnft.MSNFTP_FileReceive(auth, "foo@bar.com", self.output)
1162 # client.fileSize = 7000
1163 # loop = LoopbackCon(client, sender)
1165 # self.failUnless((client.completed and sender.completed), "send failed to complete")
1166 # self.failUnless((self.input.getvalue() == self.output.getvalue()), "saved file does not match original")