1 # Copyright (c) 2001-2005 Twisted Matrix Laboratories.
2 # Copyright (c) 2005-2006 James Bunton
3 # See LICENSE for details.
16 from twisted
.protocols
import loopback
17 from twisted
.protocols
.basic
import LineReceiver
18 from twisted
.internet
.defer
import Deferred
19 from twisted
.internet
import reactor
, main
20 from twisted
.python
import failure
, log
21 from twisted
.trial
import unittest
24 import StringIO
, sys
, urllib
, random
, struct
29 log
.startLogging(sys
.stdout
)
35 class StringIOWithoutClosing(StringIO
.StringIO
):
38 def loseConnection(self
): pass
41 def __init__(self
, con1
, con2
):
44 self
.con1ToCon2
= loopback
.LoopbackRelay(con1
)
45 self
.con2ToCon1
= loopback
.LoopbackRelay(con2
)
46 con2
.makeConnection(self
.con1ToCon2
)
47 con1
.makeConnection(self
.con2ToCon1
)
50 def doSteps(self
, steps
=1):
51 """ Returns true if the connection finished """
55 self
.con1ToCon2
.clearBuffer()
56 self
.con2ToCon1
.clearBuffer()
57 if self
.con1ToCon2
.shouldLose
:
58 self
.con1ToCon2
.clearBuffer()
61 elif self
.con2ToCon1
.shouldLose
:
73 self
.con1
.connectionLost(failure
.Failure(main
.CONNECTION_DONE
))
74 self
.con2
.connectionLost(failure
.Failure(main
.CONNECTION_DONE
))
84 class PassportTests(unittest
.TestCase
):
88 self
.deferred
= Deferred()
89 self
.deferred
.addCallback(lambda r
: self
.result
.append(r
))
90 self
.deferred
.addErrback(printError
)
93 protocol
= msn
.PassportNexus(self
.deferred
, 'https://foobar.com/somepage.quux')
95 'Content-Length' : '0',
96 'Content-Type' : 'text/html',
97 'PassportURLs' : 'DARealm=Passport.Net,DALogin=login.myserver.com/,DAReg=reg.myserver.com'
99 transport
= StringIOWithoutClosing()
100 protocol
.makeConnection(transport
)
101 protocol
.dataReceived('HTTP/1.0 200 OK\r\n')
102 for (h
,v
) in headers
.items(): protocol
.dataReceived('%s: %s\r\n' % (h
,v
))
103 protocol
.dataReceived('\r\n')
104 self
.failUnless(self
.result
[0] == "https://login.myserver.com/")
106 def _doLoginTest(self
, response
, headers
):
107 protocol
= msn
.PassportLogin(self
.deferred
,'foo@foo.com','testpass','https://foo.com/', 'a')
108 protocol
.makeConnection(StringIOWithoutClosing())
109 protocol
.dataReceived(response
)
110 for (h
,v
) in headers
.items(): protocol
.dataReceived('%s: %s\r\n' % (h
,v
))
111 protocol
.dataReceived('\r\n')
113 def testPassportLoginSuccess(self
):
115 'Content-Length' : '0',
116 'Content-Type' : 'text/html',
117 'Authentication-Info' : "Passport1.4 da-status=success,tname=MSPAuth," +
118 "tname=MSPProf,tname=MSPSec,from-PP='somekey'," +
119 "ru=http://messenger.msn.com"
121 self
._doLoginTest
('HTTP/1.1 200 OK\r\n', headers
)
122 self
.failUnless(self
.result
[0] == (msn
.LOGIN_SUCCESS
, 'somekey'))
124 def testPassportLoginFailure(self
):
126 'Content-Type' : 'text/html',
127 'WWW-Authenticate' : 'Passport1.4 da-status=failed,' +
128 'srealm=Passport.NET,ts=-3,prompt,cburl=http://host.com,' +
129 'cbtxt=the%20error%20message'
131 self
._doLoginTest
('HTTP/1.1 401 Unauthorized\r\n', headers
)
132 self
.failUnless(self
.result
[0] == (msn
.LOGIN_FAILURE
, 'the error message'))
134 def testPassportLoginRedirect(self
):
136 'Content-Type' : 'text/html',
137 'Authentication-Info' : 'Passport1.4 da-status=redir',
138 'Location' : 'https://newlogin.host.com/'
140 self
._doLoginTest
('HTTP/1.1 302 Found\r\n', headers
)
141 self
.failUnless(self
.result
[0] == (msn
.LOGIN_REDIRECT
, 'https://newlogin.host.com/', 'a'))
145 ######################
146 # Notification tests #
147 ######################
149 class DummyNotificationClient(msn
.NotificationClient
):
150 def loggedIn(self
, userHandle
, verified
):
151 if userHandle
== 'foo@bar.com' and verified
:
154 def gotProfile(self
, message
):
155 self
.state
= 'PROFILE'
157 def gotContactStatus(self
, userHandle
, code
, screenName
):
158 if code
== msn
.STATUS_AWAY
and userHandle
== "foo@bar.com" and screenName
== "Test Screen Name":
159 c
= self
.factory
.contacts
.getContact(userHandle
)
160 if c
.caps
& msn
.MSNContact
.MSNC1
and c
.msnobj
:
161 self
.state
= 'INITSTATUS'
163 def contactStatusChanged(self
, userHandle
, code
, screenName
):
164 if code
== msn
.STATUS_LUNCH
and userHandle
== "foo@bar.com" and screenName
== "Test Name":
165 self
.state
= 'NEWSTATUS'
167 def contactAvatarChanged(self
, userHandle
, hash):
168 if userHandle
== "foo@bar.com" and hash == "b6b0bc4a5171dac590c593080405921275dcf284":
169 self
.state
= 'NEWAVATAR'
170 elif self
.state
== 'NEWAVATAR' and hash == "":
171 self
.state
= 'AVATARGONE'
173 def contactPersonalChanged(self
, userHandle
, personal
):
174 if userHandle
== 'foo@bar.com' and personal
== 'My Personal Message':
175 self
.state
= 'GOTPERSONAL'
176 elif userHandle
== 'foo@bar.com' and personal
== '':
177 self
.state
= 'PERSONALGONE'
179 def contactOffline(self
, userHandle
):
180 if userHandle
== "foo@bar.com": self
.state
= 'OFFLINE'
182 def statusChanged(self
, code
):
183 if code
== msn
.STATUS_HIDDEN
: self
.state
= 'MYSTATUS'
185 def listSynchronized(self
, *args
):
186 self
.state
= 'GOTLIST'
188 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
189 self
.state
= 'GOTPHONE'
191 def userRemovedMe(self
, userHandle
):
192 c
= self
.factory
.contacts
.getContact(userHandle
)
193 if not c
: self
.state
= 'USERREMOVEDME'
195 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
196 c
= self
.factory
.contacts
.getContact(userHandle
)
197 if c
and (c
.lists | msn
.PENDING_LIST
) and (screenName
== 'Screen Name'):
198 self
.state
= 'USERADDEDME'
200 def gotSwitchboardInvitation(self
, sessionID
, host
, port
, key
, userHandle
, screenName
):
201 if sessionID
== 1234 and \
202 host
== '192.168.1.1' and \
204 key
== '123.456' and \
205 userHandle
== 'foo@foo.com' and \
206 screenName
== 'Screen Name':
207 self
.state
= 'SBINVITED'
209 def gotMSNAlert(self
, body
, action
, subscr
):
210 self
.state
= 'NOTIFICATION'
212 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
213 if inboxunread
== 1 and foldersunread
== 0:
214 self
.state
= 'INITEMAIL1'
216 self
.state
= 'INITEMAIL2'
218 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
219 if mailfrom
== 'Some Person' and fromaddr
== 'example@passport.com' and subject
== 'newsubject':
220 self
.state
= 'REALTIMEEMAIL'
222 class NotificationTests(unittest
.TestCase
):
223 """ testing the various events in NotificationClient """
226 self
.client
= DummyNotificationClient()
227 self
.client
.factory
= msn
.NotificationFactory()
228 self
.client
.state
= 'START'
234 self
.client
.lineReceived('USR 1 OK foo@bar.com 1')
235 self
.failUnless((self
.client
.state
== 'LOGIN'), 'Failed to detect successful login')
237 def testProfile(self
):
238 m
= 'MSG Hotmail Hotmail 353\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsprofile; charset=UTF-8\r\n'
239 m
+= 'LoginTime: 1016941010\r\nEmailEnabled: 1\r\nMemberIdHigh: 40000\r\nMemberIdLow: -600000000\r\nlang_preference: 1033\r\n'
240 m
+= 'preferredEmail: foo@bar.com\r\ncountry: AU\r\nPostalCode: 90210\r\nGender: M\r\nKid: 0\r\nAge:\r\nsid: 400\r\n'
241 m
+= 'kv: 2\r\nMSPAuth: 2CACCBCCADMoV8ORoz64BVwmjtksIg!kmR!Rj5tBBqEaW9hc4YnPHSOQ$$\r\n\r\n'
242 self
.client
.dataReceived(m
)
243 self
.failUnless((self
.client
.state
== 'PROFILE'), 'Failed to detect initial profile')
245 def testInitialEmailNotification(self
):
246 m
= 'MIME-Version: 1.0\r\nContent-Type: text/x-msmsgsinitialemailnotification; charset=UTF-8\r\n'
247 m
+= '\r\nInbox-Unread: 1\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
== 'INITEMAIL1'), 'Failed to detect initial email notification')
253 def testNoInitialEmailNotification(self
):
254 m
= 'MIME-Version: 1.0\r\nContent-Type: text/x-msmsgsinitialemailnotification; charset=UTF-8\r\n'
255 m
+= '\r\nInbox-Unread: 0\r\nFolders-Unread: 0\r\nInbox-URL: /cgi-bin/HoTMaiL\r\n'
256 m
+= 'Folders-URL: /cgi-bin/folders\r\nPost-URL: http://www.hotmail.com\r\n\r\n'
257 m
= 'MSG Hotmail Hotmail %s\r\n' % (str(len(m
))) + m
258 self
.client
.dataReceived(m
)
259 self
.failUnless((self
.client
.state
!= 'INITEMAIL2'), 'Detected initial email notification when I should not have')
261 def testRealtimeEmailNotification(self
):
262 m
= 'MSG Hotmail Hotmail 356\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsemailnotification; charset=UTF-8\r\n'
263 m
+= '\r\nFrom: Some Person\r\nMessage-URL: /cgi-bin/getmsg?msg=MSG1050451140.21&start=2310&len=2059&curmbox=ACTIVE\r\n'
264 m
+= 'Post-URL: https://loginnet.passport.com/ppsecure/md5auth.srf?lc=1038\r\n'
265 m
+= 'Subject: =?"us-ascii"?Q?newsubject?=\r\nDest-Folder: ACTIVE\r\nFrom-Addr: example@passport.com\r\nid: 2\r\n'
266 self
.client
.dataReceived(m
)
267 self
.failUnless((self
.client
.state
== 'REALTIMEEMAIL'), 'Failed to detect realtime email notification')
269 def testMSNAlert(self
):
270 m
= '<NOTIFICATION ver="2" id="1342902633" siteid="199999999" siteurl="http://alerts.msn.com">\r\n'
271 m
+= '<TO pid="0x0006BFFD:0x8582C0FB" name="example@passport.com"/>\r\n'
272 m
+= '<MSG pri="1" id="1342902633">\r\n'
273 m
+= '<SUBSCR url="http://g.msn.com/3ALMSNTRACKING/199999999ToastChange?http://alerts.msn.com/Alerts/MyAlerts.aspx?strela=1"/>\r\n'
274 m
+= '<ACTION url="http://g.msn.com/3ALMSNTRACKING/199999999ToastAction?http://alerts.msn.com/Alerts/MyAlerts.aspx?strela=1"/>\r\n'
275 m
+= '<BODY lang="3076" icon="">\r\n'
276 m
+= '<TEXT>utf8-encoded text</TEXT></BODY></MSG>\r\n'
277 m
+= '</NOTIFICATION>\r\n'
278 cmd
= 'NOT %s\r\n' % str(len(m
))
280 # Whee, lots of fun to test that lineReceived & dataReceived work well with input coming
281 # in in (fairly) arbitrary chunks.
282 map(self
.client
.dataReceived
, [x
+'\r\n' for x
in m
.split('\r\n')[:-1]])
283 self
.failUnless((self
.client
.state
== 'NOTIFICATION'), 'Failed to detect MSN Alert message')
285 def testListSync(self
):
286 self
.client
.makeConnection(StringIOWithoutClosing())
287 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
289 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 3" % self
.client
.currentID
,
292 "LSG Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
293 "LSG Other%20Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz",
294 "LSG More%20Other%20Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyya",
295 "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",
297 map(self
.client
.lineReceived
, lines
)
298 contacts
= self
.client
.factory
.contacts
299 contact
= contacts
.getContact('userHandle@email.com')
300 #self.failUnless(contacts.version == 100, "Invalid contact list version")
301 self
.failUnless(contact
.screenName
== 'Some Name', "Invalid screen-name for user")
302 self
.failUnless(contacts
.groups
== {'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy': 'Friends', \
303 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz': 'Other Friends', \
304 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyya': 'More Other Friends'} \
305 , "Did not get proper group list")
306 self
.failUnless(contact
.groups
== ['yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy', \
307 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz'] and \
308 contact
.lists
== 13, "Invalid contact list/group info")
309 self
.failUnless(self
.client
.state
== 'GOTLIST', "Failed to call list sync handler")
312 def testStatus(self
):
313 # Set up the contact list
314 self
.client
.makeConnection(StringIOWithoutClosing())
315 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
317 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self
.client
.currentID
,
320 "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",
322 map(self
.client
.lineReceived
, lines
)
324 msnobj
= urllib
.quote('<msnobj Creator="buddy1@hotmail.com" Size="24539" Type="3" Location="TFR2C.tmp" Friendly="AAA=" SHA1D="trC8SlFx2sWQxZMIBAWSEnXc8oQ=" SHA1C="U32o6bosZzluJq82eAtMpx5dIEI="/>')
325 t
= [('ILN 1 AWY foo@bar.com Test%20Screen%20Name 268435456 ' + msnobj
, 'INITSTATUS', 'Failed to detect initial status report'),
326 ('NLN LUN foo@bar.com Test%20Name 0', 'NEWSTATUS', 'Failed to detect contact status change'),
327 ('NLN AWY foo@bar.com Test%20Name 0 ' + msnobj
, 'NEWAVATAR', 'Failed to detect contact avatar change'),
328 ('NLN AWY foo@bar.com Test%20Name 0', 'AVATARGONE', 'Failed to detect contact avatar disappearing'),
329 ('FLN foo@bar.com', 'OFFLINE', 'Failed to detect contact signing off'),
330 ('CHG 1 HDN 0 ' + msnobj
, 'MYSTATUS', 'Failed to detect my status changing')]
332 self
.client
.lineReceived(i
[0])
333 self
.failUnless((self
.client
.state
== i
[1]), i
[2])
336 self
.client
.dataReceived('UBX foo@bar.com 72\r\n<Data><PSM>My Personal Message</PSM><CurrentMedia></CurrentMedia></Data>')
337 self
.failUnless((self
.client
.state
== 'GOTPERSONAL'), 'Failed to detect new personal message')
338 self
.client
.dataReceived('UBX foo@bar.com 0\r\n')
339 self
.failUnless((self
.client
.state
== 'PERSONALGONE'), 'Failed to detect personal message disappearing')
342 def testAsyncPhoneChange(self
):
343 c
= msn
.MSNContact(userHandle
='userHandle@email.com')
344 self
.client
.factory
.contacts
= msn
.MSNContactList()
345 self
.client
.factory
.contacts
.addContact(c
)
346 self
.client
.makeConnection(StringIOWithoutClosing())
347 self
.client
.lineReceived("BPR 101 userHandle@email.com PHH 123%20456")
348 c
= self
.client
.factory
.contacts
.getContact('userHandle@email.com')
349 self
.failUnless(self
.client
.state
== 'GOTPHONE', "Did not fire phone change callback")
350 self
.failUnless(c
.homePhone
== '123 456', "Did not update the contact's phone number")
351 self
.failUnless(self
.client
.factory
.contacts
.version
== 101, "Did not update list version")
353 def testLateBPR(self
):
355 This test makes sure that if a BPR response that was meant
356 to be part of a SYN response (but came after the last LST)
357 is received, the correct contact is updated and all is well
359 self
.client
.makeConnection(StringIOWithoutClosing())
360 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
362 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self
.client
.currentID
,
365 "LSG Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
366 "LST N=userHandle@email.com F=Some%20Name C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 13 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
369 map(self
.client
.lineReceived
, lines
)
370 contact
= self
.client
.factory
.contacts
.getContact('userHandle@email.com')
371 self
.failUnless(contact
.homePhone
== '123 456', "Did not update contact's phone number")
374 def testUserRemovedMe(self
):
375 self
.client
.factory
.contacts
= msn
.MSNContactList()
376 contact
= msn
.MSNContact(userHandle
='foo@foo.com')
377 contact
.addToList(msn
.REVERSE_LIST
)
378 self
.client
.factory
.contacts
.addContact(contact
)
379 self
.client
.lineReceived("REM 0 RL foo@foo.com")
380 self
.failUnless(self
.client
.state
== 'USERREMOVEDME', "Failed to remove user from reverse list")
382 def testUserAddedMe(self
):
383 self
.client
.factory
.contacts
= msn
.MSNContactList()
384 self
.client
.lineReceived("ADC 0 RL N=foo@foo.com F=Screen%20Name")
385 self
.failUnless(self
.client
.state
== 'USERADDEDME', "Failed to add user to reverse lise")
387 def testAsyncSwitchboardInvitation(self
):
388 self
.client
.lineReceived("RNG 1234 192.168.1.1:1863 CKI 123.456 foo@foo.com Screen%20Name")
389 self
.failUnless((self
.client
.state
== 'SBINVITED'), 'Failed to detect switchboard invitation')
392 #######################################
393 # Notification with fake server tests #
394 #######################################
396 class FakeNotificationServer(msn
.MSNEventBase
):
397 def handle_CHG(self
, params
):
400 self
.sendLine("CHG %s %s %s %s" % (params
[0], params
[1], params
[2], params
[3]))
402 def handle_BLP(self
, params
):
403 self
.sendLine("BLP %s %s 100" % (params
[0], params
[1]))
405 def handle_ADC(self
, params
):
407 list = msn
.listCodeToID
[params
[1].lower()]
408 if list == msn
.FORWARD_LIST
:
422 if userHandle
and userGuid
:
423 self
.transport
.loseConnection()
427 self
.transport
.loseConnection()
429 self
.sendLine("ADC %s FL N=%s F=%s C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx %s" % (trid
, userHandle
, screenName
, groups
))
432 raise "NotImplementedError"
435 self
.transport
.loseConnection()
436 if not params
[2].startswith("N=") and params
[2].count('@') == 1:
437 self
.transport
.loseConnection()
438 self
.sendLine("ADC %s %s %s" % (params
[0], params
[1], params
[2]))
440 def handle_REM(self
, params
):
442 self
.transport
.loseConnection()
445 trid
= int(params
[0])
446 listType
= msn
.listCodeToID
[params
[1].lower()]
448 self
.transport
.loseConnection()
449 if listType
== msn
.FORWARD_LIST
and params
[2].count('@') > 0:
450 self
.transport
.loseConnection()
451 elif listType
!= msn
.FORWARD_LIST
and params
[2].count('@') != 1:
452 self
.transport
.loseConnection()
454 self
.sendLine("REM %s %s %s" % (params
[0], params
[1], params
[2]))
456 def handle_PRP(self
, params
):
458 self
.transport
.loseConnection()
459 if params
[1] == "MFN":
460 self
.sendLine("PRP %s MFN %s" % (params
[0], params
[2]))
462 # Only friendly names are implemented
463 self
.transport
.loseConnection()
465 def handle_UUX(self
, params
):
467 self
.transport
.loseConnection()
471 self
.currentMessage
= msn
.MSNMessage(length
=l
, userHandle
=params
[0], screenName
="UUX", specialMessage
=True)
474 self
.sendLine("UUX %s 0" % params
[0])
476 def checkMessage(self
, message
):
477 if message
.specialMessage
:
478 if message
.screenName
== "UUX":
479 self
.sendLine("UUX %s 0" % message
.userHandle
)
483 def handle_XFR(self
, params
):
485 self
.transport
.loseConnection()
487 if params
[1] != "SB":
488 self
.transport
.loseConnection()
490 self
.sendLine("XFR %s SB 129.129.129.129:1234 CKI SomeSecret" % params
[0])
494 class FakeNotificationClient(msn
.NotificationClient
):
495 def doStatusChange(self
):
496 def testcb((status
,)):
497 if status
== msn
.STATUS_AWAY
:
499 self
.transport
.loseConnection()
500 d
= self
.changeStatus(msn
.STATUS_AWAY
)
501 d
.addCallback(testcb
)
503 def doPrivacyMode(self
):
505 if priv
.upper() == 'AL':
507 self
.transport
.loseConnection()
508 d
= self
.setPrivacyMode(True)
509 d
.addCallback(testcb
)
511 def doAddContactFL(self
):
512 def testcb((listType
, userGuid
, userHandle
, screenName
)):
513 if listType
& msn
.FORWARD_LIST
and \
514 userGuid
== "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" and \
515 userHandle
== "foo@bar.com" and \
516 screenName
== "foo@bar.com" and \
517 self
.factory
.contacts
.getContact(userHandle
):
519 self
.transport
.loseConnection()
520 d
= self
.addContact(msn
.FORWARD_LIST
, "foo@bar.com")
521 d
.addCallback(testcb
)
523 def doAddContactAL(self
):
524 def testcb((listType
, userGuid
, userHandle
, screenName
)):
525 if listType
& msn
.ALLOW_LIST
and \
526 userHandle
== "foo@bar.com" and \
527 not userGuid
and not screenName
and \
528 self
.factory
.contacts
.getContact(userHandle
):
530 self
.transport
.loseConnection()
531 d
= self
.addContact(msn
.ALLOW_LIST
, "foo@bar.com")
532 d
.addCallback(testcb
)
534 def doRemContactFL(self
):
535 def testcb((listType
, userHandle
, groupID
)):
536 if listType
& msn
.FORWARD_LIST
and \
537 userHandle
== "foo@bar.com":
539 self
.transport
.loseConnection()
540 d
= self
.remContact(msn
.FORWARD_LIST
, "foo@bar.com")
541 d
.addCallback(testcb
)
543 def doRemContactAL(self
):
544 def testcb((listType
, userHandle
, groupID
)):
545 if listType
& msn
.ALLOW_LIST
and \
546 userHandle
== "foo@bar.com":
548 self
.transport
.loseConnection()
549 d
= self
.remContact(msn
.ALLOW_LIST
, "foo@bar.com")
550 d
.addCallback(testcb
)
552 def doScreenNameChange(self
):
555 self
.transport
.loseConnection()
556 d
= self
.changeScreenName("Some new name")
557 d
.addCallback(testcb
)
559 def doPersonalChange(self
, personal
):
560 def testcb((checkPersonal
,)):
561 if checkPersonal
== personal
:
563 self
.transport
.loseConnection()
564 d
= self
.changePersonalMessage(personal
)
565 d
.addCallback(testcb
)
567 def doAvatarChange(self
, data
):
570 self
.transport
.loseConnection()
571 d
= self
.changeAvatar(data
, True)
572 d
.addCallback(testcb
)
574 def doRequestSwitchboard(self
):
575 def testcb((host
, port
, key
)):
576 if host
== "129.129.129.129" and port
== 1234 and key
== "SomeSecret":
578 self
.transport
.loseConnection()
579 d
= self
.requestSwitchboardServer()
580 d
.addCallback(testcb
)
582 class FakeServerNotificationTests(unittest
.TestCase
):
583 """ tests the NotificationClient against a fake server. """
586 self
.client
= FakeNotificationClient()
587 self
.client
.factory
= msn
.NotificationFactory()
588 self
.client
.test
= 'FAIL'
589 self
.server
= FakeNotificationServer()
590 self
.loop
= LoopbackCon(self
.client
, self
.server
)
593 self
.loop
.disconnect()
595 def testChangeStatus(self
):
596 self
.client
.doStatusChange()
597 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
598 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change status properly')
600 def testSetPrivacyMode(self
):
601 self
.client
.factory
.contacts
= msn
.MSNContactList()
602 self
.client
.doPrivacyMode()
603 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
604 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change privacy mode')
606 def testSyncList(self
):
607 self
.client
.doSyncList()
608 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
609 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to synchronise list')
610 testSyncList
.skip
= "Will do after list versions."
612 def testAddContactFL(self
):
613 self
.client
.factory
.contacts
= msn
.MSNContactList()
614 self
.client
.doAddContactFL()
615 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
616 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to add contact to forward list')
618 def testAddContactAL(self
):
619 self
.client
.factory
.contacts
= msn
.MSNContactList()
620 self
.client
.doAddContactAL()
621 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
622 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to add contact to allow list')
624 def testRemContactFL(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
.FORWARD_LIST
))
627 self
.client
.doRemContactFL()
628 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
629 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to remove contact from forward list')
631 def testRemContactAL(self
):
632 self
.client
.factory
.contacts
= msn
.MSNContactList()
633 self
.client
.factory
.contacts
.addContact(msn
.MSNContact(userGuid
="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", userHandle
="foo@bar.com", screenName
="Some guy", lists
=msn
.ALLOW_LIST
))
634 self
.client
.doRemContactAL()
635 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
636 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to remove contact from allow list')
638 def testChangedScreenName(self
):
639 self
.client
.doScreenNameChange()
640 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
641 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change screen name properly')
643 def testChangePersonal1(self
):
644 self
.client
.doPersonalChange("Some personal message")
645 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
646 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change personal message properly')
648 def testChangePersonal2(self
):
649 self
.client
.doPersonalChange("")
650 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
651 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change personal message properly')
653 def testChangeAvatar(self
):
654 self
.client
.doAvatarChange("DATADATADATADATA")
655 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
656 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change avatar properly')
658 def testRequestSwitchboard(self
):
659 self
.client
.doRequestSwitchboard()
660 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
661 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to request switchboard')
664 #################################
665 # Notification challenges tests #
666 #################################
668 class DummyChallengeNotificationServer(msn
.MSNEventBase
):
669 def doChallenge(self
, challenge
, response
):
671 self
.response
= response
672 self
.sendLine("CHL 0 " + challenge
)
674 def checkMessage(self
, message
):
675 if message
.message
== self
.response
:
677 self
.transport
.loseConnection()
680 def handle_QRY(self
, params
):
682 if len(params
) == 3 and params
[1] == "PROD0090YUAUV{2B" and params
[2] == "32":
684 self
.currentMessage
= msn
.MSNMessage(length
=32, userHandle
="QRY", screenName
="QRY", specialMessage
=True)
687 self
.transport
.loseConnection()
689 class DummyChallengeNotificationClient(msn
.NotificationClient
):
690 def connectionMade(self
):
691 msn
.MSNEventBase
.connectionMade(self
)
693 def handle_CHL(self
, params
):
694 msn
.NotificationClient
.handle_CHL(self
, params
)
695 self
.transport
.loseConnection()
698 class NotificationChallengeTests(unittest
.TestCase
):
699 """ tests the responses to the CHLs the server sends """
702 self
.client
= DummyChallengeNotificationClient()
703 self
.server
= DummyChallengeNotificationServer()
704 self
.loop
= LoopbackCon(self
.client
, self
.server
)
707 self
.loop
.disconnect()
709 def testChallenges(self
):
710 challenges
= [('13038318816579321232', 'b01c13020e374d4fa20abfad6981b7a9'),
711 ('23055170411503520698', 'ae906c3f2946d25e7da1b08b0b247659'),
712 ('37819769320541083311', 'db79d37dadd9031bef996893321da480'),
713 ('93662730714769834295', 'd619dfbb1414004d34d0628766636568'),
714 ('31154116582196216093', '95e96c4f8cfdba6f065c8869b5e984e9')]
715 for challenge
, response
in challenges
:
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 self
.pingCheckerStart()
734 def sendLine(self
, line
):
735 msn
.NotificationClient
.sendLine(self
, line
)
738 self
.transport
.loseConnection() # But not for real, just to end the test
740 def connectionLost(self
, reason
):
742 self
.state
= 'DISCONNECTED'
744 class NotificationPingTests(unittest
.TestCase
):
745 """ tests pinging in the NotificationClient class """
749 self
.client
= DummyPingNotificationClient()
750 self
.server
= DummyPingNotificationServer()
751 self
.client
.factory
= msn
.NotificationFactory()
752 self
.server
.factory
= msn
.NotificationFactory()
753 self
.client
.state
= 'CONNECTED'
754 self
.client
.count
= 0
755 self
.loop
= LoopbackCon(self
.client
, self
.server
)
760 self
.loop
.disconnect()
762 def testPingGood(self
):
763 self
.server
.good
= True
764 self
.loop
.doSteps(100)
765 self
.failUnless((self
.client
.state
== 'CONNECTED'), 'Should be connected.')
767 def testPingBad(self
):
768 self
.server
.good
= False
769 self
.loop
.doSteps(100)
770 self
.failUnless((self
.client
.state
== 'DISCONNECTED'), 'Should be disconnected.')
775 ###########################
776 # Switchboard basic tests #
777 ###########################
779 class DummySwitchboardServer(msn
.MSNEventBase
):
780 def handle_USR(self
, params
):
782 self
.transport
.loseConnection()
783 if params
[1] == 'foo@bar.com' and params
[2] == 'somekey':
784 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
786 def handle_ANS(self
, params
):
788 self
.transport
.loseConnection()
789 if params
[1] == 'foo@bar.com' and params
[2] == 'somekey' and params
[3] == 'someSID':
790 self
.sendLine("ANS %s OK" % params
[0])
792 def handle_CAL(self
, params
):
794 self
.transport
.loseConnection()
795 if params
[1] == 'friend@hotmail.com':
796 self
.sendLine("CAL %s RINGING 1111122" % params
[0])
798 self
.transport
.loseConnection()
800 def checkMessage(self
, message
):
801 if message
.message
== 'Hi how are you today?':
802 self
.sendLine("ACK " + message
.userHandle
) # Relies on TRID getting stored in userHandle trick
804 self
.transport
.loseConnection()
807 class DummySwitchboardClient(msn
.SwitchboardClient
):
809 self
.state
= 'LOGGEDIN'
810 self
.transport
.loseConnection()
812 def gotChattingUsers(self
, users
):
813 if users
== {'fred@hotmail.com': 'fred', 'jack@email.com': 'jack has a nickname!'}:
814 self
.state
= 'GOTCHATTINGUSERS'
816 def userJoined(self
, userHandle
, screenName
):
817 if userHandle
== "friend@hotmail.com" and screenName
== "friend nickname":
818 self
.state
= 'USERJOINED'
820 def userLeft(self
, userHandle
):
821 if userHandle
== "friend@hotmail.com":
822 self
.state
= 'USERLEFT'
824 def gotContactTyping(self
, message
):
825 if message
.userHandle
== 'foo@bar.com':
826 self
.state
= 'USERTYPING'
828 def gotMessage(self
, message
):
829 if message
.userHandle
== 'friend@hotmail.com' and \
830 message
.screenName
== 'Friend Nickname' and \
831 message
.message
== 'Hello.':
832 self
.state
= 'GOTMESSAGE'
834 def doSendInvite(self
):
837 self
.state
= 'INVITESUCCESS'
838 self
.transport
.loseConnection()
839 d
= self
.inviteUser('friend@hotmail.com')
840 d
.addCallback(testcb
)
842 def doSendMessage(self
):
844 self
.state
= 'MESSAGESUCCESS'
845 self
.transport
.loseConnection()
847 m
.setHeader("Content-Type", "text/plain; charset=UTF-8")
848 m
.message
= 'Hi how are you today?'
849 m
.ack
= msn
.MSNMessage
.MESSAGE_ACK
850 d
= self
.sendMessage(m
)
851 d
.addCallback(testcb
)
854 class SwitchboardBasicTests(unittest
.TestCase
):
855 """ Tests basic functionality of switchboard sessions """
857 self
.client
= DummySwitchboardClient()
858 self
.client
.state
= 'START'
859 self
.client
.userHandle
= 'foo@bar.com'
860 self
.client
.key
= 'somekey'
861 self
.client
.sessionID
= 'someSID'
862 self
.server
= DummySwitchboardServer()
863 self
.loop
= LoopbackCon(self
.client
, self
.server
)
866 self
.loop
.disconnect()
868 def _testSB(self
, reply
):
869 self
.client
.reply
= reply
870 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
871 self
.failUnless((self
.client
.state
== 'LOGGEDIN'), 'Failed to login with reply='+str(reply
))
879 def testChattingUsers(self
):
880 lines
= ["IRO 1 1 2 fred@hotmail.com fred",
881 "IRO 1 2 2 jack@email.com jack%20has%20a%20nickname%21"]
883 self
.client
.lineReceived(line
)
884 self
.failUnless((self
.client
.state
== 'GOTCHATTINGUSERS'), 'Failed to get chatting users')
886 def testUserJoined(self
):
887 self
.client
.lineReceived("JOI friend@hotmail.com friend%20nickname")
888 self
.failUnless((self
.client
.state
== 'USERJOINED'), 'Failed to notice user joining')
890 def testUserLeft(self
):
891 self
.client
.lineReceived("BYE friend@hotmail.com")
892 self
.failUnless((self
.client
.state
== 'USERLEFT'), 'Failed to notice user leaving')
894 def testTypingCheck(self
):
895 m
= 'MSG foo@bar.com Foo 80\r\n'
896 m
+= 'MIME-Version: 1.0\r\n'
897 m
+= 'Content-Type: text/x-msmsgscontrol\r\n'
898 m
+= 'TypingUser: foo@bar\r\n'
900 self
.client
.dataReceived(m
)
901 self
.failUnless((self
.client
.state
== 'USERTYPING'), 'Failed to detect typing notification')
903 def testGotMessage(self
):
904 m
= 'MSG friend@hotmail.com Friend%20Nickname 68\r\n'
905 m
+= 'MIME-Version: 1.0\r\n'
906 m
+= 'Content-Type: text/plain; charset=UTF-8\r\n'
908 self
.client
.dataReceived(m
)
909 self
.failUnless((self
.client
.state
== 'GOTMESSAGE'), 'Failed to detect message')
911 def testInviteUser(self
):
912 self
.client
.connectionMade
= lambda: None
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
.connectionMade
= lambda: None
919 self
.client
.doSendMessage()
920 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
921 self
.failUnless((self
.client
.state
== 'MESSAGESUCCESS'), 'Failed to send message')
928 class DummySwitchboardP2PServerHelper(msn
.MSNEventBase
):
929 def __init__(self
, server
):
930 msn
.MSNEventBase
.__init
__(self
)
933 def handle_USR(self
, params
):
935 self
.transport
.loseConnection()
936 self
.userHandle
= params
[1]
937 if params
[1] == 'foo1@bar.com' and params
[2] == 'somekey1':
938 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
939 if params
[1] == 'foo2@bar.com' and params
[2] == 'somekey2':
940 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
942 def checkMessage(self
, message
):
945 def gotMessage(self
, message
):
946 message
.userHandle
= self
.userHandle
947 message
.screenName
= self
.userHandle
948 self
.server
.gotMessage(message
, self
)
950 def sendMessage(self
, message
):
951 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
952 self
.sendLine("MSG %s %s %s" % (message
.userHandle
, message
.screenName
, message
.length
))
953 self
.sendLine('MIME-Version: %s' % message
.getHeader('MIME-Version'))
954 self
.sendLine('Content-Type: %s' % message
.getHeader('Content-Type'))
955 for header
in [h
for h
in message
.headers
.items() if h
[0].lower() not in ('mime-version','content-type')]:
956 self
.sendLine("%s: %s" % (header
[0], header
[1]))
957 self
.transport
.write("\r\n")
958 self
.transport
.write(message
.message
)
961 class DummySwitchboardP2PServer
:
966 c
= DummySwitchboardP2PServerHelper(self
)
967 self
.clients
.append(c
)
970 def gotMessage(self
, message
, sender
):
971 for c
in self
.clients
:
973 c
.sendMessage(message
)
975 class DummySwitchboardP2PClient(msn
.SwitchboardClient
):
976 def gotMessage(self
, message
):
977 if message
.message
== "Test Message" and message
.userHandle
== "foo1@bar.com":
978 self
.status
= "GOTMESSAGE"
980 def gotFileReceive(self
, fileReceive
):
981 self
.fileReceive
= fileReceive
983 class SwitchboardP2PTests(unittest
.TestCase
):
985 self
.server
= DummySwitchboardP2PServer()
986 self
.client1
= DummySwitchboardP2PClient()
987 self
.client1
.key
= 'somekey1'
988 self
.client1
.userHandle
= 'foo1@bar.com'
989 self
.client2
= DummySwitchboardP2PClient()
990 self
.client2
.key
= 'somekey2'
991 self
.client2
.userHandle
= 'foo2@bar.com'
992 self
.client2
.status
= "INIT"
993 self
.loop1
= LoopbackCon(self
.client1
, self
.server
.newClient())
994 self
.loop2
= LoopbackCon(self
.client2
, self
.server
.newClient())
997 self
.loop1
.disconnect()
998 self
.loop2
.disconnect()
1000 def _loop(self
, steps
=1):
1001 for i
in xrange(steps
):
1002 self
.loop1
.doSteps(1)
1003 self
.loop2
.doSteps(1)
1005 def testMessage(self
):
1006 self
.client1
.sendMessage(msn
.MSNMessage(message
='Test Message'))
1008 self
.failUnless((self
.client2
.status
== "GOTMESSAGE"), "Fake switchboard server not working.")
1010 def _generateData(self
):
1012 for i
in xrange(3000):
1013 data
+= struct
.pack("<L", random
.randint(0, sys
.maxint
))
1016 def testAvatars(self
):
1017 self
.gotAvatar
= False
1019 # Set up the avatar for client1
1020 imageData
= self
._generateData
()
1021 self
.client1
.msnobj
= msn
.MSNObject()
1022 self
.client1
.msnobj
.setData('foo1@bar.com', imageData
)
1023 self
.client1
.msnobj
.makeText()
1025 # Make client2 request the avatar
1026 def avatarCallback((data
,)):
1027 self
.gotAvatar
= (data
== imageData
)
1028 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', msnobj
=self
.client1
.msnobj
)
1029 d
= self
.client2
.sendAvatarRequest(msnContact
)
1030 d
.addCallback(avatarCallback
)
1032 # Let them do their thing
1035 # Check that client2 got the avatar
1036 self
.failUnless((self
.gotAvatar
), "Failed to transfer avatar")
1038 def testFilesHappyPath(self
):
1039 fileData
= self
._generateData
()
1040 self
.gotFile
= False
1042 # Send the file (client2->client1)
1043 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1044 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1045 def accepted((yes
,)):
1047 fileSend
.write(fileData
)
1050 raise "TransferDeclined"
1052 raise "TransferError"
1053 d
.addCallback(accepted
)
1054 d
.addErrback(failed
)
1056 # Let the request get pushed to client1
1061 self
.gotFile
= (data
== fileData
)
1062 fileBuffer
= msn
.StringBuffer(finished
)
1063 fileReceive
= self
.client1
.fileReceive
1064 self
.failUnless((fileReceive
.filename
== "myfile.txt" and fileReceive
.filesize
== len(fileData
)), "Filename or length wrong.")
1065 fileReceive
.accept(fileBuffer
)
1070 self
.failUnless((self
.gotFile
), "Failed to transfer file")
1072 def testFilesHappyChunkedPath(self
):
1073 fileData
= self
._generateData
()
1074 self
.gotFile
= False
1076 # Send the file (client2->client1)
1077 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1078 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1079 def accepted((yes
,)):
1081 fileSend
.write(fileData
[:len(fileData
)/2])
1082 fileSend
.write(fileData
[len(fileData
)/2:])
1085 raise "TransferDeclined"
1087 raise "TransferError"
1088 d
.addCallback(accepted
)
1089 d
.addErrback(failed
)
1091 # Let the request get pushed to client1
1096 self
.gotFile
= (data
== fileData
)
1097 fileBuffer
= msn
.StringBuffer(finished
)
1098 fileReceive
= self
.client1
.fileReceive
1099 self
.failUnless((fileReceive
.filename
== "myfile.txt" and fileReceive
.filesize
== len(fileData
)), "Filename or length wrong.")
1100 fileReceive
.accept(fileBuffer
)
1105 self
.failUnless((self
.gotFile
), "Failed to transfer file")
1107 def testTwoFilesSequential(self
):
1108 self
.testFilesHappyPath()
1109 self
.testFilesHappyPath()
1111 def testFilesDeclinePath(self
):
1112 fileData
= self
._generateData
()
1113 self
.gotDecline
= False
1115 # Send the file (client2->client1)
1116 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1117 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1118 def accepted((yes
,)):
1119 self
.failUnless((not yes
), "Failed to understand a decline.")
1120 self
.gotDecline
= True
1122 raise "TransferError"
1123 d
.addCallback(accepted
)
1124 d
.addErrback(failed
)
1126 # Let the request get pushed to client1
1130 fileReceive
= self
.client1
.fileReceive
1131 fileReceive
.reject()
1133 # Let the decline get pushed to client2
1136 self
.failUnless((self
.gotDecline
), "Failed to understand a decline, ignored.")
1143 class FileTransferTestCase(unittest
.TestCase
):
1144 """ test FileSend against FileReceive """
1145 skip
= "Not implemented"
1148 self
.input = StringIOWithoutClosing()
1149 self
.input.writelines(['a'] * 7000)
1151 self
.output
= StringIOWithoutClosing()
1157 def testFileTransfer(self
):
1159 sender
= msnft
.MSNFTP_FileSend(self
.input)
1161 sender
.fileSize
= 7000
1162 client
= msnft
.MSNFTP_FileReceive(auth
, "foo@bar.com", self
.output
)
1163 client
.fileSize
= 7000
1164 loop
= LoopbackCon(client
, sender
)
1166 self
.failUnless((client
.completed
and sender
.completed
), "send failed to complete")
1167 self
.failUnless((self
.input.getvalue() == self
.output
.getvalue()), "saved file does not match original")