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
)
49 self
.con2
.makeConnection(self
.con1ToCon2
)
50 self
.con1
.makeConnection(self
.con2ToCon1
)
53 def doSteps(self
, steps
=1):
54 """ Returns true if the connection finished """
58 self
.con1ToCon2
.clearBuffer()
59 self
.con2ToCon1
.clearBuffer()
60 if self
.con1ToCon2
.shouldLose
:
61 self
.con1ToCon2
.clearBuffer()
64 elif self
.con2ToCon1
.shouldLose
:
76 self
.con1
.connectionLost(failure
.Failure(main
.CONNECTION_DONE
))
77 self
.con2
.connectionLost(failure
.Failure(main
.CONNECTION_DONE
))
87 class PassportTests(unittest
.TestCase
):
91 self
.deferred
= Deferred()
92 self
.deferred
.addCallback(lambda r
: self
.result
.append(r
))
93 self
.deferred
.addErrback(printError
)
96 protocol
= msn
.PassportNexus(self
.deferred
, 'https://foobar.com/somepage.quux')
98 'Content-Length' : '0',
99 'Content-Type' : 'text/html',
100 'PassportURLs' : 'DARealm=Passport.Net,DALogin=login.myserver.com/,DAReg=reg.myserver.com'
102 transport
= StringIOWithoutClosing()
103 protocol
.makeConnection(transport
)
104 protocol
.dataReceived('HTTP/1.0 200 OK\r\n')
105 for (h
,v
) in headers
.items(): protocol
.dataReceived('%s: %s\r\n' % (h
,v
))
106 protocol
.dataReceived('\r\n')
107 self
.failUnless(self
.result
[0] == "https://login.myserver.com/")
109 def _doLoginTest(self
, response
, headers
):
110 protocol
= msn
.PassportLogin(self
.deferred
,'foo@foo.com','testpass','https://foo.com/', 'a')
111 protocol
.makeConnection(StringIOWithoutClosing())
112 protocol
.dataReceived(response
)
113 for (h
,v
) in headers
.items(): protocol
.dataReceived('%s: %s\r\n' % (h
,v
))
114 protocol
.dataReceived('\r\n')
116 def testPassportLoginSuccess(self
):
118 'Content-Length' : '0',
119 'Content-Type' : 'text/html',
120 'Authentication-Info' : "Passport1.4 da-status=success,tname=MSPAuth," +
121 "tname=MSPProf,tname=MSPSec,from-PP='somekey'," +
122 "ru=http://messenger.msn.com"
124 self
._doLoginTest
('HTTP/1.1 200 OK\r\n', headers
)
125 self
.failUnless(self
.result
[0] == (msn
.LOGIN_SUCCESS
, 'somekey'))
127 def testPassportLoginFailure(self
):
129 'Content-Type' : 'text/html',
130 'WWW-Authenticate' : 'Passport1.4 da-status=failed,' +
131 'srealm=Passport.NET,ts=-3,prompt,cburl=http://host.com,' +
132 'cbtxt=the%20error%20message'
134 self
._doLoginTest
('HTTP/1.1 401 Unauthorized\r\n', headers
)
135 self
.failUnless(self
.result
[0] == (msn
.LOGIN_FAILURE
, 'the error message'))
137 def testPassportLoginRedirect(self
):
139 'Content-Type' : 'text/html',
140 'Authentication-Info' : 'Passport1.4 da-status=redir',
141 'Location' : 'https://newlogin.host.com/'
143 self
._doLoginTest
('HTTP/1.1 302 Found\r\n', headers
)
144 self
.failUnless(self
.result
[0] == (msn
.LOGIN_REDIRECT
, 'https://newlogin.host.com/', 'a'))
148 ######################
149 # Notification tests #
150 ######################
152 class DummyNotificationClient(msn
.NotificationClient
):
153 def loggedIn(self
, userHandle
, verified
):
154 if userHandle
== 'foo@bar.com' and verified
:
157 def gotProfile(self
, message
):
158 self
.state
= 'PROFILE'
160 def gotContactStatus(self
, userHandle
, code
, screenName
):
161 if code
== msn
.STATUS_AWAY
and userHandle
== "foo@bar.com" and screenName
== "Test Screen Name":
162 c
= self
.factory
.contacts
.getContact(userHandle
)
163 if c
.caps
& msn
.MSNContact
.MSNC1
and c
.msnobj
:
164 self
.state
= 'INITSTATUS'
166 def contactStatusChanged(self
, userHandle
, code
, screenName
):
167 if code
== msn
.STATUS_LUNCH
and userHandle
== "foo@bar.com" and screenName
== "Test Name":
168 self
.state
= 'NEWSTATUS'
170 def contactAvatarChanged(self
, userHandle
, hash):
171 if userHandle
== "foo@bar.com" and hash == "b6b0bc4a5171dac590c593080405921275dcf284":
172 self
.state
= 'NEWAVATAR'
173 elif self
.state
== 'NEWAVATAR' and hash == "":
174 self
.state
= 'AVATARGONE'
176 def contactPersonalChanged(self
, userHandle
, personal
):
177 if userHandle
== 'foo@bar.com' and personal
== 'My Personal Message':
178 self
.state
= 'GOTPERSONAL'
179 elif userHandle
== 'foo@bar.com' and personal
== '':
180 self
.state
= 'PERSONALGONE'
182 def contactOffline(self
, userHandle
):
183 if userHandle
== "foo@bar.com": self
.state
= 'OFFLINE'
185 def statusChanged(self
, code
):
186 if code
== msn
.STATUS_HIDDEN
: self
.state
= 'MYSTATUS'
188 def listSynchronized(self
, *args
):
189 self
.state
= 'GOTLIST'
191 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
192 self
.state
= 'GOTPHONE'
194 def userRemovedMe(self
, userHandle
):
195 c
= self
.factory
.contacts
.getContact(userHandle
)
196 if not c
: self
.state
= 'USERREMOVEDME'
198 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
199 c
= self
.factory
.contacts
.getContact(userHandle
)
200 if c
and (c
.lists | msn
.PENDING_LIST
) and (screenName
== 'Screen Name'):
201 self
.state
= 'USERADDEDME'
203 def gotSwitchboardInvitation(self
, sessionID
, host
, port
, key
, userHandle
, screenName
):
204 if sessionID
== 1234 and \
205 host
== '192.168.1.1' and \
207 key
== '123.456' and \
208 userHandle
== 'foo@foo.com' and \
209 screenName
== 'Screen Name':
210 self
.state
= 'SBINVITED'
212 def gotMSNAlert(self
, body
, action
, subscr
):
213 self
.state
= 'NOTIFICATION'
215 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
216 if inboxunread
== 1 and foldersunread
== 0:
217 self
.state
= 'INITEMAIL1'
219 self
.state
= 'INITEMAIL2'
221 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
222 if mailfrom
== 'Some Person' and fromaddr
== 'example@passport.com' and subject
== 'newsubject':
223 self
.state
= 'REALTIMEEMAIL'
225 class NotificationTests(unittest
.TestCase
):
226 """ testing the various events in NotificationClient """
229 self
.client
= DummyNotificationClient()
230 self
.client
.factory
= msn
.NotificationFactory()
231 msn
.MSNEventBase
.connectionMade(self
.client
)
232 self
.client
.state
= 'START'
238 self
.client
.lineReceived('USR 1 OK foo@bar.com 1')
239 self
.failUnless((self
.client
.state
== 'LOGIN'), 'Failed to detect successful login')
241 def testProfile(self
):
242 m
= 'MSG Hotmail Hotmail 353\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsprofile; charset=UTF-8\r\n'
243 m
+= 'LoginTime: 1016941010\r\nEmailEnabled: 1\r\nMemberIdHigh: 40000\r\nMemberIdLow: -600000000\r\nlang_preference: 1033\r\n'
244 m
+= 'preferredEmail: foo@bar.com\r\ncountry: AU\r\nPostalCode: 90210\r\nGender: M\r\nKid: 0\r\nAge:\r\nsid: 400\r\n'
245 m
+= 'kv: 2\r\nMSPAuth: 2CACCBCCADMoV8ORoz64BVwmjtksIg!kmR!Rj5tBBqEaW9hc4YnPHSOQ$$\r\n\r\n'
246 self
.client
.dataReceived(m
)
247 self
.failUnless((self
.client
.state
== 'PROFILE'), 'Failed to detect initial profile')
249 def testInitialEmailNotification(self
):
250 m
= 'MIME-Version: 1.0\r\nContent-Type: text/x-msmsgsinitialemailnotification; charset=UTF-8\r\n'
251 m
+= '\r\nInbox-Unread: 1\r\nFolders-Unread: 0\r\nInbox-URL: /cgi-bin/HoTMaiL\r\n'
252 m
+= 'Folders-URL: /cgi-bin/folders\r\nPost-URL: http://www.hotmail.com\r\n\r\n'
253 m
= 'MSG Hotmail Hotmail %s\r\n' % (str(len(m
))) + m
254 self
.client
.dataReceived(m
)
255 self
.failUnless((self
.client
.state
== 'INITEMAIL1'), 'Failed to detect initial email notification')
257 def testNoInitialEmailNotification(self
):
258 m
= 'MIME-Version: 1.0\r\nContent-Type: text/x-msmsgsinitialemailnotification; charset=UTF-8\r\n'
259 m
+= '\r\nInbox-Unread: 0\r\nFolders-Unread: 0\r\nInbox-URL: /cgi-bin/HoTMaiL\r\n'
260 m
+= 'Folders-URL: /cgi-bin/folders\r\nPost-URL: http://www.hotmail.com\r\n\r\n'
261 m
= 'MSG Hotmail Hotmail %s\r\n' % (str(len(m
))) + m
262 self
.client
.dataReceived(m
)
263 self
.failUnless((self
.client
.state
!= 'INITEMAIL2'), 'Detected initial email notification when I should not have')
265 def testRealtimeEmailNotification(self
):
266 m
= 'MSG Hotmail Hotmail 356\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsemailnotification; charset=UTF-8\r\n'
267 m
+= '\r\nFrom: Some Person\r\nMessage-URL: /cgi-bin/getmsg?msg=MSG1050451140.21&start=2310&len=2059&curmbox=ACTIVE\r\n'
268 m
+= 'Post-URL: https://loginnet.passport.com/ppsecure/md5auth.srf?lc=1038\r\n'
269 m
+= 'Subject: =?"us-ascii"?Q?newsubject?=\r\nDest-Folder: ACTIVE\r\nFrom-Addr: example@passport.com\r\nid: 2\r\n'
270 self
.client
.dataReceived(m
)
271 self
.failUnless((self
.client
.state
== 'REALTIMEEMAIL'), 'Failed to detect realtime email notification')
273 def testMSNAlert(self
):
274 m
= '<NOTIFICATION ver="2" id="1342902633" siteid="199999999" siteurl="http://alerts.msn.com">\r\n'
275 m
+= '<TO pid="0x0006BFFD:0x8582C0FB" name="example@passport.com"/>\r\n'
276 m
+= '<MSG pri="1" id="1342902633">\r\n'
277 m
+= '<SUBSCR url="http://g.msn.com/3ALMSNTRACKING/199999999ToastChange?http://alerts.msn.com/Alerts/MyAlerts.aspx?strela=1"/>\r\n'
278 m
+= '<ACTION url="http://g.msn.com/3ALMSNTRACKING/199999999ToastAction?http://alerts.msn.com/Alerts/MyAlerts.aspx?strela=1"/>\r\n'
279 m
+= '<BODY lang="3076" icon="">\r\n'
280 m
+= '<TEXT>utf8-encoded text</TEXT></BODY></MSG>\r\n'
281 m
+= '</NOTIFICATION>\r\n'
282 cmd
= 'NOT %s\r\n' % str(len(m
))
284 # Whee, lots of fun to test that lineReceived & dataReceived work well with input coming
285 # in in (fairly) arbitrary chunks.
286 map(self
.client
.dataReceived
, [x
+'\r\n' for x
in m
.split('\r\n')[:-1]])
287 self
.failUnless((self
.client
.state
== 'NOTIFICATION'), 'Failed to detect MSN Alert message')
289 def testListSync(self
):
290 self
.client
.makeConnection(StringIOWithoutClosing())
291 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
293 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 3" % self
.client
.currentID
,
296 "LSG Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
297 "LSG Other%20Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz",
298 "LSG More%20Other%20Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyya",
299 "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",
301 map(self
.client
.lineReceived
, lines
)
302 contacts
= self
.client
.factory
.contacts
303 contact
= contacts
.getContact('userHandle@email.com')
304 #self.failUnless(contacts.version == 100, "Invalid contact list version")
305 self
.failUnless(contact
.screenName
== 'Some Name', "Invalid screen-name for user")
306 self
.failUnless(contacts
.groups
== {'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy': 'Friends', \
307 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz': 'Other Friends', \
308 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyya': 'More Other Friends'} \
309 , "Did not get proper group list")
310 self
.failUnless(contact
.groups
== ['yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy', \
311 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz'] and \
312 contact
.lists
== 13, "Invalid contact list/group info")
313 self
.failUnless(self
.client
.state
== 'GOTLIST', "Failed to call list sync handler")
316 def testStatus(self
):
317 # Set up the contact list
318 self
.client
.makeConnection(StringIOWithoutClosing())
319 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
321 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self
.client
.currentID
,
324 "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",
326 map(self
.client
.lineReceived
, lines
)
328 msnobj
= urllib
.quote('<msnobj Creator="buddy1@hotmail.com" Size="24539" Type="3" Location="TFR2C.tmp" Friendly="AAA=" SHA1D="trC8SlFx2sWQxZMIBAWSEnXc8oQ=" SHA1C="U32o6bosZzluJq82eAtMpx5dIEI="/>')
329 t
= [('ILN 1 AWY foo@bar.com Test%20Screen%20Name 268435456 ' + msnobj
, 'INITSTATUS', 'Failed to detect initial status report'),
330 ('NLN LUN foo@bar.com Test%20Name 0', 'NEWSTATUS', 'Failed to detect contact status change'),
331 ('NLN AWY foo@bar.com Test%20Name 0 ' + msnobj
, 'NEWAVATAR', 'Failed to detect contact avatar change'),
332 ('NLN AWY foo@bar.com Test%20Name 0', 'AVATARGONE', 'Failed to detect contact avatar disappearing'),
333 ('FLN foo@bar.com', 'OFFLINE', 'Failed to detect contact signing off'),
334 ('CHG 1 HDN 0 ' + msnobj
, 'MYSTATUS', 'Failed to detect my status changing')]
336 self
.client
.lineReceived(i
[0])
337 self
.failUnless((self
.client
.state
== i
[1]), i
[2])
340 self
.client
.dataReceived('UBX foo@bar.com 72\r\n<Data><PSM>My Personal Message</PSM><CurrentMedia></CurrentMedia></Data>')
341 self
.failUnless((self
.client
.state
== 'GOTPERSONAL'), 'Failed to detect new personal message')
342 self
.client
.dataReceived('UBX foo@bar.com 0\r\n')
343 self
.failUnless((self
.client
.state
== 'PERSONALGONE'), 'Failed to detect personal message disappearing')
346 def testAsyncPhoneChange(self
):
347 c
= msn
.MSNContact(userHandle
='userHandle@email.com')
348 self
.client
.factory
.contacts
= msn
.MSNContactList()
349 self
.client
.factory
.contacts
.addContact(c
)
350 self
.client
.makeConnection(StringIOWithoutClosing())
351 self
.client
.lineReceived("BPR 101 userHandle@email.com PHH 123%20456")
352 c
= self
.client
.factory
.contacts
.getContact('userHandle@email.com')
353 self
.failUnless(self
.client
.state
== 'GOTPHONE', "Did not fire phone change callback")
354 self
.failUnless(c
.homePhone
== '123 456', "Did not update the contact's phone number")
355 self
.failUnless(self
.client
.factory
.contacts
.version
== 101, "Did not update list version")
357 def testLateBPR(self
):
359 This test makes sure that if a BPR response that was meant
360 to be part of a SYN response (but came after the last LST)
361 is received, the correct contact is updated and all is well
363 self
.client
.makeConnection(StringIOWithoutClosing())
364 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
366 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self
.client
.currentID
,
369 "LSG Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
370 "LST N=userHandle@email.com F=Some%20Name C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 13 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
373 map(self
.client
.lineReceived
, lines
)
374 contact
= self
.client
.factory
.contacts
.getContact('userHandle@email.com')
375 self
.failUnless(contact
.homePhone
== '123 456', "Did not update contact's phone number")
378 def testUserRemovedMe(self
):
379 self
.client
.factory
.contacts
= msn
.MSNContactList()
380 contact
= msn
.MSNContact(userHandle
='foo@foo.com')
381 contact
.addToList(msn
.REVERSE_LIST
)
382 self
.client
.factory
.contacts
.addContact(contact
)
383 self
.client
.lineReceived("REM 0 RL foo@foo.com")
384 self
.failUnless(self
.client
.state
== 'USERREMOVEDME', "Failed to remove user from reverse list")
386 def testUserAddedMe(self
):
387 self
.client
.factory
.contacts
= msn
.MSNContactList()
388 self
.client
.lineReceived("ADC 0 RL N=foo@foo.com F=Screen%20Name")
389 self
.failUnless(self
.client
.state
== 'USERADDEDME', "Failed to add user to reverse lise")
391 def testAsyncSwitchboardInvitation(self
):
392 self
.client
.lineReceived("RNG 1234 192.168.1.1:1863 CKI 123.456 foo@foo.com Screen%20Name")
393 self
.failUnless((self
.client
.state
== 'SBINVITED'), 'Failed to detect switchboard invitation')
396 #######################################
397 # Notification with fake server tests #
398 #######################################
400 class FakeNotificationServer(msn
.MSNEventBase
):
401 def handle_CHG(self
, params
):
404 self
.sendLine("CHG %s %s %s %s" % (params
[0], params
[1], params
[2], params
[3]))
406 def handle_BLP(self
, params
):
407 self
.sendLine("BLP %s %s 100" % (params
[0], params
[1]))
409 def handle_ADC(self
, params
):
411 list = msn
.listCodeToID
[params
[1].lower()]
412 if list == msn
.FORWARD_LIST
:
426 if userHandle
and userGuid
:
427 self
.transport
.loseConnection()
431 self
.transport
.loseConnection()
433 self
.sendLine("ADC %s FL N=%s F=%s C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx %s" % (trid
, userHandle
, screenName
, groups
))
436 raise "NotImplementedError"
439 self
.transport
.loseConnection()
440 if not params
[2].startswith("N=") and params
[2].count('@') == 1:
441 self
.transport
.loseConnection()
442 self
.sendLine("ADC %s %s %s" % (params
[0], params
[1], params
[2]))
444 def handle_REM(self
, params
):
446 self
.transport
.loseConnection()
449 trid
= int(params
[0])
450 listType
= msn
.listCodeToID
[params
[1].lower()]
452 self
.transport
.loseConnection()
453 if listType
== msn
.FORWARD_LIST
and params
[2].count('@') > 0:
454 self
.transport
.loseConnection()
455 elif listType
!= msn
.FORWARD_LIST
and params
[2].count('@') != 1:
456 self
.transport
.loseConnection()
458 self
.sendLine("REM %s %s %s" % (params
[0], params
[1], params
[2]))
460 def handle_PRP(self
, params
):
462 self
.transport
.loseConnection()
463 if params
[1] == "MFN":
464 self
.sendLine("PRP %s MFN %s" % (params
[0], params
[2]))
466 # Only friendly names are implemented
467 self
.transport
.loseConnection()
469 def handle_UUX(self
, params
):
471 self
.transport
.loseConnection()
475 self
.currentMessage
= msn
.MSNMessage(length
=l
, userHandle
=params
[0], screenName
="UUX", specialMessage
=True)
478 self
.sendLine("UUX %s 0" % params
[0])
480 def checkMessage(self
, message
):
481 if message
.specialMessage
:
482 if message
.screenName
== "UUX":
483 self
.sendLine("UUX %s 0" % message
.userHandle
)
487 def handle_XFR(self
, params
):
489 self
.transport
.loseConnection()
491 if params
[1] != "SB":
492 self
.transport
.loseConnection()
494 self
.sendLine("XFR %s SB 129.129.129.129:1234 CKI SomeSecret" % params
[0])
498 class FakeNotificationClient(msn
.NotificationClient
):
499 def doStatusChange(self
):
500 def testcb((status
,)):
501 if status
== msn
.STATUS_AWAY
:
503 self
.transport
.loseConnection()
504 d
= self
.changeStatus(msn
.STATUS_AWAY
)
505 d
.addCallback(testcb
)
507 def doPrivacyMode(self
):
509 if priv
.upper() == 'AL':
511 self
.transport
.loseConnection()
512 d
= self
.setPrivacyMode(True)
513 d
.addCallback(testcb
)
515 def doAddContactFL(self
):
516 def testcb((listType
, userGuid
, userHandle
, screenName
)):
517 if listType
& msn
.FORWARD_LIST
and \
518 userGuid
== "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" and \
519 userHandle
== "foo@bar.com" and \
520 screenName
== "foo@bar.com" and \
521 self
.factory
.contacts
.getContact(userHandle
):
523 self
.transport
.loseConnection()
524 d
= self
.addContact(msn
.FORWARD_LIST
, "foo@bar.com")
525 d
.addCallback(testcb
)
527 def doAddContactAL(self
):
528 def testcb((listType
, userGuid
, userHandle
, screenName
)):
529 if listType
& msn
.ALLOW_LIST
and \
530 userHandle
== "foo@bar.com" and \
531 not userGuid
and not screenName
and \
532 self
.factory
.contacts
.getContact(userHandle
):
534 self
.transport
.loseConnection()
535 d
= self
.addContact(msn
.ALLOW_LIST
, "foo@bar.com")
536 d
.addCallback(testcb
)
538 def doRemContactFL(self
):
539 def testcb((listType
, userHandle
, groupID
)):
540 if listType
& msn
.FORWARD_LIST
and \
541 userHandle
== "foo@bar.com":
543 self
.transport
.loseConnection()
544 d
= self
.remContact(msn
.FORWARD_LIST
, "foo@bar.com")
545 d
.addCallback(testcb
)
547 def doRemContactAL(self
):
548 def testcb((listType
, userHandle
, groupID
)):
549 if listType
& msn
.ALLOW_LIST
and \
550 userHandle
== "foo@bar.com":
552 self
.transport
.loseConnection()
553 d
= self
.remContact(msn
.ALLOW_LIST
, "foo@bar.com")
554 d
.addCallback(testcb
)
556 def doScreenNameChange(self
):
559 self
.transport
.loseConnection()
560 d
= self
.changeScreenName("Some new name")
561 d
.addCallback(testcb
)
563 def doPersonalChange(self
, personal
):
564 def testcb((checkPersonal
,)):
565 if checkPersonal
== personal
:
567 self
.transport
.loseConnection()
568 d
= self
.changePersonalMessage(personal
)
569 d
.addCallback(testcb
)
571 def doAvatarChange(self
, data
):
574 self
.transport
.loseConnection()
575 d
= self
.changeAvatar(data
, True)
576 d
.addCallback(testcb
)
578 def doRequestSwitchboard(self
):
579 def testcb((host
, port
, key
)):
580 if host
== "129.129.129.129" and port
== 1234 and key
== "SomeSecret":
582 self
.transport
.loseConnection()
583 d
= self
.requestSwitchboardServer()
584 d
.addCallback(testcb
)
586 class FakeServerNotificationTests(unittest
.TestCase
):
587 """ tests the NotificationClient against a fake server. """
590 self
.client
= FakeNotificationClient()
591 self
.client
.factory
= msn
.NotificationFactory()
592 self
.client
.test
= 'FAIL'
593 self
.server
= FakeNotificationServer()
594 self
.loop
= LoopbackCon(self
.client
, self
.server
)
597 self
.loop
.disconnect()
599 def testChangeStatus(self
):
600 self
.client
.doStatusChange()
601 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
602 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change status properly')
604 def testSetPrivacyMode(self
):
605 self
.client
.factory
.contacts
= msn
.MSNContactList()
606 self
.client
.doPrivacyMode()
607 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
608 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change privacy mode')
610 def testAddContactFL(self
):
611 self
.client
.factory
.contacts
= msn
.MSNContactList()
612 self
.client
.doAddContactFL()
613 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
614 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to add contact to forward list')
616 def testAddContactAL(self
):
617 self
.client
.factory
.contacts
= msn
.MSNContactList()
618 self
.client
.doAddContactAL()
619 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
620 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to add contact to allow list')
622 def testRemContactFL(self
):
623 self
.client
.factory
.contacts
= msn
.MSNContactList()
624 self
.client
.factory
.contacts
.addContact(msn
.MSNContact(userGuid
="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", userHandle
="foo@bar.com", screenName
="Some guy", lists
=msn
.FORWARD_LIST
))
625 self
.client
.doRemContactFL()
626 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
627 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to remove contact from forward list')
629 def testRemContactAL(self
):
630 self
.client
.factory
.contacts
= msn
.MSNContactList()
631 self
.client
.factory
.contacts
.addContact(msn
.MSNContact(userGuid
="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", userHandle
="foo@bar.com", screenName
="Some guy", lists
=msn
.ALLOW_LIST
))
632 self
.client
.doRemContactAL()
633 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
634 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to remove contact from allow list')
636 def testChangedScreenName(self
):
637 self
.client
.doScreenNameChange()
638 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
639 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change screen name properly')
641 def testChangePersonal1(self
):
642 self
.client
.doPersonalChange("Some personal message")
643 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
644 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change personal message properly')
646 def testChangePersonal2(self
):
647 self
.client
.doPersonalChange("")
648 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
649 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change personal message properly')
651 def testChangeAvatar(self
):
652 self
.client
.doAvatarChange("DATADATADATADATA")
653 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
654 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change avatar properly')
656 def testRequestSwitchboard(self
):
657 self
.client
.doRequestSwitchboard()
658 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
659 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to request switchboard')
662 #################################
663 # Notification challenges tests #
664 #################################
666 class DummyChallengeNotificationServer(msn
.MSNEventBase
):
667 def doChallenge(self
, challenge
, response
):
669 self
.response
= response
670 self
.sendLine("CHL 0 " + challenge
)
672 def checkMessage(self
, message
):
673 if message
.message
== self
.response
:
675 self
.transport
.loseConnection()
678 def handle_QRY(self
, params
):
680 if len(params
) == 3 and params
[1] == "PROD0090YUAUV{2B" and params
[2] == "32":
682 self
.currentMessage
= msn
.MSNMessage(length
=32, userHandle
="QRY", screenName
="QRY", specialMessage
=True)
685 self
.transport
.loseConnection()
687 class DummyChallengeNotificationClient(msn
.NotificationClient
):
688 def connectionMade(self
):
689 msn
.MSNEventBase
.connectionMade(self
)
691 def handle_CHL(self
, params
):
692 msn
.NotificationClient
.handle_CHL(self
, params
)
693 self
.transport
.loseConnection()
696 class NotificationChallengeTests(unittest
.TestCase
):
697 """ tests the responses to the CHLs the server sends """
700 self
.client
= DummyChallengeNotificationClient()
701 self
.server
= DummyChallengeNotificationServer()
702 self
.loop
= LoopbackCon(self
.client
, self
.server
)
705 self
.loop
.disconnect()
707 def testChallenges(self
):
708 challenges
= [('13038318816579321232', 'b01c13020e374d4fa20abfad6981b7a9'),
709 ('23055170411503520698', 'ae906c3f2946d25e7da1b08b0b247659'),
710 ('37819769320541083311', 'db79d37dadd9031bef996893321da480'),
711 ('93662730714769834295', 'd619dfbb1414004d34d0628766636568'),
712 ('31154116582196216093', '95e96c4f8cfdba6f065c8869b5e984e9')]
713 for challenge
, response
in challenges
:
714 self
.loop
.reconnect()
715 self
.server
.doChallenge(challenge
, response
)
716 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
717 self
.failUnless((self
.server
.state
== 'PASS'), 'Incorrect challenge response.')
720 ###########################
721 # Notification ping tests #
722 ###########################
724 class DummyPingNotificationServer(LineReceiver
):
725 def lineReceived(self
, line
):
726 if line
.startswith("PNG") and self
.good
:
727 self
.sendLine("QNG 50")
729 class DummyPingNotificationClient(msn
.NotificationClient
):
730 def connectionMade(self
):
731 msn
.MSNEventBase
.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
.doSendInvite()
913 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
914 self
.failUnless((self
.client
.state
== 'INVITESUCCESS'), 'Failed to invite user')
916 def testSendMessage(self
):
917 self
.client
.doSendMessage()
918 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
919 self
.failUnless((self
.client
.state
== 'MESSAGESUCCESS'), 'Failed to send message')
926 class DummySwitchboardP2PServerHelper(msn
.MSNEventBase
):
927 def __init__(self
, server
):
928 msn
.MSNEventBase
.__init
__(self
)
931 def handle_USR(self
, params
):
933 self
.transport
.loseConnection()
934 self
.userHandle
= params
[1]
935 if params
[1] == 'foo1@bar.com' and params
[2] == 'somekey1':
936 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
937 if params
[1] == 'foo2@bar.com' and params
[2] == 'somekey2':
938 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
940 def checkMessage(self
, message
):
943 def gotMessage(self
, message
):
944 message
.userHandle
= self
.userHandle
945 message
.screenName
= self
.userHandle
946 self
.server
.gotMessage(message
, self
)
948 def sendMessage(self
, message
):
949 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
950 self
.sendLine("MSG %s %s %s" % (message
.userHandle
, message
.screenName
, message
.length
))
951 self
.sendLine('MIME-Version: %s' % message
.getHeader('MIME-Version'))
952 self
.sendLine('Content-Type: %s' % message
.getHeader('Content-Type'))
953 for header
in [h
for h
in message
.headers
.items() if h
[0].lower() not in ('mime-version','content-type')]:
954 self
.sendLine("%s: %s" % (header
[0], header
[1]))
955 self
.transport
.write("\r\n")
956 self
.transport
.write(message
.message
)
959 class DummySwitchboardP2PServer
:
964 c
= DummySwitchboardP2PServerHelper(self
)
965 self
.clients
.append(c
)
968 def gotMessage(self
, message
, sender
):
969 for c
in self
.clients
:
971 c
.sendMessage(message
)
973 class DummySwitchboardP2PClient(msn
.SwitchboardClient
):
974 def gotMessage(self
, message
):
975 if message
.message
== "Test Message" and message
.userHandle
== "foo1@bar.com":
976 self
.status
= "GOTMESSAGE"
978 def gotFileReceive(self
, fileReceive
):
979 self
.fileReceive
= fileReceive
981 class SwitchboardP2PTests(unittest
.TestCase
):
983 self
.server
= DummySwitchboardP2PServer()
984 self
.client1
= DummySwitchboardP2PClient()
985 self
.client1
.key
= 'somekey1'
986 self
.client1
.userHandle
= 'foo1@bar.com'
987 self
.client2
= DummySwitchboardP2PClient()
988 self
.client2
.key
= 'somekey2'
989 self
.client2
.userHandle
= 'foo2@bar.com'
990 self
.client2
.status
= "INIT"
991 self
.loop1
= LoopbackCon(self
.client1
, self
.server
.newClient())
992 self
.loop2
= LoopbackCon(self
.client2
, self
.server
.newClient())
995 self
.loop1
.disconnect()
996 self
.loop2
.disconnect()
998 def _loop(self
, steps
=1):
999 for i
in xrange(steps
):
1000 self
.loop1
.doSteps(1)
1001 self
.loop2
.doSteps(1)
1003 def testMessage(self
):
1004 self
.client1
.sendMessage(msn
.MSNMessage(message
='Test Message'))
1006 self
.failUnless((self
.client2
.status
== "GOTMESSAGE"), "Fake switchboard server not working.")
1008 def _generateData(self
):
1010 for i
in xrange(3000):
1011 data
+= struct
.pack("<L", random
.randint(0, sys
.maxint
))
1014 def testAvatars(self
):
1015 self
.gotAvatar
= False
1017 # Set up the avatar for client1
1018 imageData
= self
._generateData
()
1019 self
.client1
.msnobj
= msn
.MSNObject()
1020 self
.client1
.msnobj
.setData('foo1@bar.com', imageData
)
1021 self
.client1
.msnobj
.makeText()
1023 # Make client2 request the avatar
1024 def avatarCallback((data
,)):
1025 self
.gotAvatar
= (data
== imageData
)
1026 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', msnobj
=self
.client1
.msnobj
)
1027 d
= self
.client2
.sendAvatarRequest(msnContact
)
1028 d
.addCallback(avatarCallback
)
1030 # Let them do their thing
1033 # Check that client2 got the avatar
1034 self
.failUnless((self
.gotAvatar
), "Failed to transfer avatar")
1036 def testFilesHappyPath(self
):
1037 fileData
= self
._generateData
()
1038 self
.gotFile
= False
1040 # Send the file (client2->client1)
1041 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1042 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1043 def accepted((yes
,)):
1045 fileSend
.write(fileData
)
1048 raise "TransferDeclined"
1050 raise "TransferError"
1051 d
.addCallback(accepted
)
1052 d
.addErrback(failed
)
1054 # Let the request get pushed to client1
1059 self
.gotFile
= (data
== fileData
)
1060 fileBuffer
= msn
.StringBuffer(finished
)
1061 fileReceive
= self
.client1
.fileReceive
1062 self
.failUnless((fileReceive
.filename
== "myfile.txt" and fileReceive
.filesize
== len(fileData
)), "Filename or length wrong.")
1063 fileReceive
.accept(fileBuffer
)
1068 self
.failUnless((self
.gotFile
), "Failed to transfer file")
1070 def testFilesHappyChunkedPath(self
):
1071 fileData
= self
._generateData
()
1072 self
.gotFile
= False
1074 # Send the file (client2->client1)
1075 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1076 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1077 def accepted((yes
,)):
1079 fileSend
.write(fileData
[:len(fileData
)/2])
1080 fileSend
.write(fileData
[len(fileData
)/2:])
1083 raise "TransferDeclined"
1085 raise "TransferError"
1086 d
.addCallback(accepted
)
1087 d
.addErrback(failed
)
1089 # Let the request get pushed to client1
1094 self
.gotFile
= (data
== fileData
)
1095 fileBuffer
= msn
.StringBuffer(finished
)
1096 fileReceive
= self
.client1
.fileReceive
1097 self
.failUnless((fileReceive
.filename
== "myfile.txt" and fileReceive
.filesize
== len(fileData
)), "Filename or length wrong.")
1098 fileReceive
.accept(fileBuffer
)
1103 self
.failUnless((self
.gotFile
), "Failed to transfer file")
1105 def testTwoFilesSequential(self
):
1106 self
.testFilesHappyPath()
1107 self
.testFilesHappyPath()
1109 def testFilesDeclinePath(self
):
1110 fileData
= self
._generateData
()
1111 self
.gotDecline
= False
1113 # Send the file (client2->client1)
1114 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1115 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1116 def accepted((yes
,)):
1117 self
.failUnless((not yes
), "Failed to understand a decline.")
1118 self
.gotDecline
= True
1120 raise "TransferError"
1121 d
.addCallback(accepted
)
1122 d
.addErrback(failed
)
1124 # Let the request get pushed to client1
1128 fileReceive
= self
.client1
.fileReceive
1129 fileReceive
.reject()
1131 # Let the decline get pushed to client2
1134 self
.failUnless((self
.gotDecline
), "Failed to understand a decline, ignored.")
1141 #class FileTransferTestCase(unittest.TestCase):
1142 # """ test FileSend against FileReceive """
1143 # skip = "Not implemented"
1146 # self.input = StringIOWithoutClosing()
1147 # self.input.writelines(['a'] * 7000)
1148 # self.input.seek(0)
1149 # self.output = StringIOWithoutClosing()
1151 # def tearDown(self):
1153 # self.output = None
1155 # def testFileTransfer(self):
1157 # sender = msnft.MSNFTP_FileSend(self.input)
1158 # sender.auth = auth
1159 # sender.fileSize = 7000
1160 # client = msnft.MSNFTP_FileReceive(auth, "foo@bar.com", self.output)
1161 # client.fileSize = 7000
1162 # loop = LoopbackCon(client, sender)
1164 # self.failUnless((client.completed and sender.completed), "send failed to complete")
1165 # self.failUnless((self.input.getvalue() == self.output.getvalue()), "saved file does not match original")