1 # Copyright (c) 2001-2005 Twisted Matrix Laboratories.
2 # Copyright (c) 2005 James Bunton
3 # See LICENSE for details.
10 from twisted
.protocols
import loopback
11 from twisted
.protocols
.basic
import LineReceiver
12 from twisted
.internet
.defer
import Deferred
13 from twisted
.internet
import reactor
, main
14 from twisted
.python
import failure
15 from twisted
.trial
import unittest
18 import StringIO
, sys
, urllib
, random
, struct
26 class StringIOWithoutClosing(StringIO
.StringIO
):
29 def loseConnection(self
): pass
32 def __init__(self
, con1
, con2
):
35 self
.con1ToCon2
= loopback
.LoopbackRelay(con1
)
36 self
.con2ToCon1
= loopback
.LoopbackRelay(con2
)
37 con2
.makeConnection(self
.con1ToCon2
)
38 con1
.makeConnection(self
.con2ToCon1
)
41 def doSteps(self
, steps
=1):
42 """ Returns true if the connection finished """
46 self
.con1ToCon2
.clearBuffer()
47 self
.con2ToCon1
.clearBuffer()
48 if self
.con1ToCon2
.shouldLose
:
49 self
.con1ToCon2
.clearBuffer()
52 elif self
.con2ToCon1
.shouldLose
:
64 self
.con1
.connectionLost(failure
.Failure(main
.CONNECTION_DONE
))
65 self
.con2
.connectionLost(failure
.Failure(main
.CONNECTION_DONE
))
75 class PassportTests(unittest
.TestCase
):
79 self
.deferred
= Deferred()
80 self
.deferred
.addCallback(lambda r
: self
.result
.append(r
))
81 self
.deferred
.addErrback(printError
)
84 protocol
= msn
.PassportNexus(self
.deferred
, 'https://foobar.com/somepage.quux')
86 'Content-Length' : '0',
87 'Content-Type' : 'text/html',
88 'PassportURLs' : 'DARealm=Passport.Net,DALogin=login.myserver.com/,DAReg=reg.myserver.com'
90 transport
= StringIOWithoutClosing()
91 protocol
.makeConnection(transport
)
92 protocol
.dataReceived('HTTP/1.0 200 OK\r\n')
93 for (h
,v
) in headers
.items(): protocol
.dataReceived('%s: %s\r\n' % (h
,v
))
94 protocol
.dataReceived('\r\n')
95 self
.failUnless(self
.result
[0] == "https://login.myserver.com/")
97 def _doLoginTest(self
, response
, headers
):
98 protocol
= msn
.PassportLogin(self
.deferred
,'foo@foo.com','testpass','https://foo.com/', 'a')
99 protocol
.makeConnection(StringIOWithoutClosing())
100 protocol
.dataReceived(response
)
101 for (h
,v
) in headers
.items(): protocol
.dataReceived('%s: %s\r\n' % (h
,v
))
102 protocol
.dataReceived('\r\n')
104 def testPassportLoginSuccess(self
):
106 'Content-Length' : '0',
107 'Content-Type' : 'text/html',
108 'Authentication-Info' : "Passport1.4 da-status=success,tname=MSPAuth," +
109 "tname=MSPProf,tname=MSPSec,from-PP='somekey'," +
110 "ru=http://messenger.msn.com"
112 self
._doLoginTest
('HTTP/1.1 200 OK\r\n', headers
)
113 self
.failUnless(self
.result
[0] == (msn
.LOGIN_SUCCESS
, 'somekey'))
115 def testPassportLoginFailure(self
):
117 'Content-Type' : 'text/html',
118 'WWW-Authenticate' : 'Passport1.4 da-status=failed,' +
119 'srealm=Passport.NET,ts=-3,prompt,cburl=http://host.com,' +
120 'cbtxt=the%20error%20message'
122 self
._doLoginTest
('HTTP/1.1 401 Unauthorized\r\n', headers
)
123 self
.failUnless(self
.result
[0] == (msn
.LOGIN_FAILURE
, 'the error message'))
125 def testPassportLoginRedirect(self
):
127 'Content-Type' : 'text/html',
128 'Authentication-Info' : 'Passport1.4 da-status=redir',
129 'Location' : 'https://newlogin.host.com/'
131 self
._doLoginTest
('HTTP/1.1 302 Found\r\n', headers
)
132 self
.failUnless(self
.result
[0] == (msn
.LOGIN_REDIRECT
, 'https://newlogin.host.com/', 'a'))
136 ######################
137 # Notification tests #
138 ######################
140 class DummyNotificationClient(msn
.NotificationClient
):
141 def loggedIn(self
, userHandle
, verified
):
142 if userHandle
== 'foo@bar.com' and verified
:
145 def gotProfile(self
, message
):
146 self
.state
= 'PROFILE'
148 def gotContactStatus(self
, userHandle
, code
, screenName
):
149 if code
== msn
.STATUS_AWAY
and userHandle
== "foo@bar.com" and screenName
== "Test Screen Name":
150 c
= self
.factory
.contacts
.getContact(userHandle
)
151 if c
.caps
& msn
.MSNContact
.MSNC1
and c
.msnobj
:
152 self
.state
= 'INITSTATUS'
154 def contactStatusChanged(self
, userHandle
, code
, screenName
):
155 if code
== msn
.STATUS_LUNCH
and userHandle
== "foo@bar.com" and screenName
== "Test Name":
156 self
.state
= 'NEWSTATUS'
158 def contactAvatarChanged(self
, userHandle
, hash):
159 if userHandle
== "foo@bar.com" and hash == "b6b0bc4a5171dac590c593080405921275dcf284":
160 self
.state
= 'NEWAVATAR'
161 elif self
.state
== 'NEWAVATAR' and hash == "":
162 self
.state
= 'AVATARGONE'
164 def contactPersonalChanged(self
, userHandle
, personal
):
165 if userHandle
== 'foo@bar.com' and personal
== 'My Personal Message':
166 self
.state
= 'GOTPERSONAL'
167 elif userHandle
== 'foo@bar.com' and personal
== '':
168 self
.state
= 'PERSONALGONE'
170 def contactOffline(self
, userHandle
):
171 if userHandle
== "foo@bar.com": self
.state
= 'OFFLINE'
173 def statusChanged(self
, code
):
174 if code
== msn
.STATUS_HIDDEN
: self
.state
= 'MYSTATUS'
176 def listSynchronized(self
, *args
):
177 self
.state
= 'GOTLIST'
179 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
180 self
.state
= 'GOTPHONE'
182 def userRemovedMe(self
, userHandle
):
183 c
= self
.factory
.contacts
.getContact(userHandle
)
184 if not c
: self
.state
= 'USERREMOVEDME'
186 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
187 c
= self
.factory
.contacts
.getContact(userHandle
)
188 if c
and (c
.lists | msn
.PENDING_LIST
) and (screenName
== 'Screen Name'):
189 self
.state
= 'USERADDEDME'
191 def gotSwitchboardInvitation(self
, sessionID
, host
, port
, key
, userHandle
, screenName
):
192 if sessionID
== 1234 and \
193 host
== '192.168.1.1' and \
195 key
== '123.456' and \
196 userHandle
== 'foo@foo.com' and \
197 screenName
== 'Screen Name':
198 self
.state
= 'SBINVITED'
200 def gotMSNAlert(self
, body
, action
, subscr
):
201 self
.state
= 'NOTIFICATION'
203 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
204 if inboxunread
== 1 and foldersunread
== 0:
205 self
.state
= 'INITEMAIL1'
207 self
.state
= 'INITEMAIL2'
209 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
210 if mailfrom
== 'Some Person' and fromaddr
== 'example@passport.com' and subject
== 'newsubject':
211 self
.state
= 'REALTIMEEMAIL'
213 class NotificationTests(unittest
.TestCase
):
214 """ testing the various events in NotificationClient """
217 self
.client
= DummyNotificationClient()
218 self
.client
.factory
= msn
.NotificationFactory()
219 self
.client
.state
= 'START'
225 self
.client
.lineReceived('USR 1 OK foo@bar.com 1')
226 self
.failUnless((self
.client
.state
== 'LOGIN'), 'Failed to detect successful login')
228 def testProfile(self
):
229 m
= 'MSG Hotmail Hotmail 353\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsprofile; charset=UTF-8\r\n'
230 m
+= 'LoginTime: 1016941010\r\nEmailEnabled: 1\r\nMemberIdHigh: 40000\r\nMemberIdLow: -600000000\r\nlang_preference: 1033\r\n'
231 m
+= 'preferredEmail: foo@bar.com\r\ncountry: AU\r\nPostalCode: 90210\r\nGender: M\r\nKid: 0\r\nAge:\r\nsid: 400\r\n'
232 m
+= 'kv: 2\r\nMSPAuth: 2CACCBCCADMoV8ORoz64BVwmjtksIg!kmR!Rj5tBBqEaW9hc4YnPHSOQ$$\r\n\r\n'
233 self
.client
.dataReceived(m
)
234 self
.failUnless((self
.client
.state
== 'PROFILE'), 'Failed to detect initial profile')
236 def testInitialEmailNotification(self
):
237 m
= 'MIME-Version: 1.0\r\nContent-Type: text/x-msmsgsinitialemailnotification; charset=UTF-8\r\n'
238 m
+= '\r\nInbox-Unread: 1\r\nFolders-Unread: 0\r\nInbox-URL: /cgi-bin/HoTMaiL\r\n'
239 m
+= 'Folders-URL: /cgi-bin/folders\r\nPost-URL: http://www.hotmail.com\r\n\r\n'
240 m
= 'MSG Hotmail Hotmail %s\r\n' % (str(len(m
))) + m
241 self
.client
.dataReceived(m
)
242 self
.failUnless((self
.client
.state
== 'INITEMAIL1'), 'Failed to detect initial email notification')
244 def testNoInitialEmailNotification(self
):
245 m
= 'MIME-Version: 1.0\r\nContent-Type: text/x-msmsgsinitialemailnotification; charset=UTF-8\r\n'
246 m
+= '\r\nInbox-Unread: 0\r\nFolders-Unread: 0\r\nInbox-URL: /cgi-bin/HoTMaiL\r\n'
247 m
+= 'Folders-URL: /cgi-bin/folders\r\nPost-URL: http://www.hotmail.com\r\n\r\n'
248 m
= 'MSG Hotmail Hotmail %s\r\n' % (str(len(m
))) + m
249 self
.client
.dataReceived(m
)
250 self
.failUnless((self
.client
.state
!= 'INITEMAIL2'), 'Detected initial email notification when I should not have')
252 def testRealtimeEmailNotification(self
):
253 m
= 'MSG Hotmail Hotmail 356\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsemailnotification; charset=UTF-8\r\n'
254 m
+= '\r\nFrom: Some Person\r\nMessage-URL: /cgi-bin/getmsg?msg=MSG1050451140.21&start=2310&len=2059&curmbox=ACTIVE\r\n'
255 m
+= 'Post-URL: https://loginnet.passport.com/ppsecure/md5auth.srf?lc=1038\r\n'
256 m
+= 'Subject: =?"us-ascii"?Q?newsubject?=\r\nDest-Folder: ACTIVE\r\nFrom-Addr: example@passport.com\r\nid: 2\r\n'
257 self
.client
.dataReceived(m
)
258 self
.failUnless((self
.client
.state
== 'REALTIMEEMAIL'), 'Failed to detect realtime email notification')
260 def testMSNAlert(self
):
261 m
= '<NOTIFICATION ver="2" id="1342902633" siteid="199999999" siteurl="http://alerts.msn.com">\r\n'
262 m
+= '<TO pid="0x0006BFFD:0x8582C0FB" name="example@passport.com"/>\r\n'
263 m
+= '<MSG pri="1" id="1342902633">\r\n'
264 m
+= '<SUBSCR url="http://g.msn.com/3ALMSNTRACKING/199999999ToastChange?http://alerts.msn.com/Alerts/MyAlerts.aspx?strela=1"/>\r\n'
265 m
+= '<ACTION url="http://g.msn.com/3ALMSNTRACKING/199999999ToastAction?http://alerts.msn.com/Alerts/MyAlerts.aspx?strela=1"/>\r\n'
266 m
+= '<BODY lang="3076" icon="">\r\n'
267 m
+= '<TEXT>utf8-encoded text</TEXT></BODY></MSG>\r\n'
268 m
+= '</NOTIFICATION>\r\n'
269 cmd
= 'NOT %s\r\n' % str(len(m
))
271 # Whee, lots of fun to test that lineReceived & dataReceived work well with input coming
272 # in in (fairly) arbitrary chunks.
273 map(self
.client
.dataReceived
, [x
+'\r\n' for x
in m
.split('\r\n')[:-1]])
274 self
.failUnless((self
.client
.state
== 'NOTIFICATION'), 'Failed to detect MSN Alert message')
276 def testListSync(self
):
277 self
.client
.makeConnection(StringIOWithoutClosing())
278 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
280 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 3" % self
.client
.currentID
,
283 "LSG Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
284 "LSG Other%20Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz",
285 "LSG More%20Other%20Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyya",
286 "LST N=userHandle@email.com F=Some%20Name C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 13 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy,yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz",
288 map(self
.client
.lineReceived
, lines
)
289 contacts
= self
.client
.factory
.contacts
290 contact
= contacts
.getContact('userHandle@email.com')
291 #self.failUnless(contacts.version == 100, "Invalid contact list version")
292 self
.failUnless(contact
.screenName
== 'Some Name', "Invalid screen-name for user")
293 self
.failUnless(contacts
.groups
== {'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy': 'Friends', \
294 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz': 'Other Friends', \
295 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyya': 'More Other Friends'} \
296 , "Did not get proper group list")
297 self
.failUnless(contact
.groups
== ['yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy', \
298 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz'] and \
299 contact
.lists
== 13, "Invalid contact list/group info")
300 self
.failUnless(self
.client
.state
== 'GOTLIST', "Failed to call list sync handler")
303 def testStatus(self
):
304 # Set up the contact list
305 self
.client
.makeConnection(StringIOWithoutClosing())
306 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
308 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self
.client
.currentID
,
311 "LST N=foo@bar.com F=Some%20Name C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 13 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy,yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz",
313 map(self
.client
.lineReceived
, lines
)
315 msnobj
= urllib
.quote('<msnobj Creator="buddy1@hotmail.com" Size="24539" Type="3" Location="TFR2C.tmp" Friendly="AAA=" SHA1D="trC8SlFx2sWQxZMIBAWSEnXc8oQ=" SHA1C="U32o6bosZzluJq82eAtMpx5dIEI="/>')
316 t
= [('ILN 1 AWY foo@bar.com Test%20Screen%20Name 268435456 ' + msnobj
, 'INITSTATUS', 'Failed to detect initial status report'),
317 ('NLN LUN foo@bar.com Test%20Name 0', 'NEWSTATUS', 'Failed to detect contact status change'),
318 ('NLN AWY foo@bar.com Test%20Name 0 ' + msnobj
, 'NEWAVATAR', 'Failed to detect contact avatar change'),
319 ('NLN AWY foo@bar.com Test%20Name 0', 'AVATARGONE', 'Failed to detect contact avatar disappearing'),
320 ('FLN foo@bar.com', 'OFFLINE', 'Failed to detect contact signing off'),
321 ('CHG 1 HDN 0 ' + msnobj
, 'MYSTATUS', 'Failed to detect my status changing')]
323 self
.client
.lineReceived(i
[0])
324 self
.failUnless((self
.client
.state
== i
[1]), i
[2])
327 self
.client
.dataReceived('UBX foo@bar.com 72\r\n<Data><PSM>My Personal Message</PSM><CurrentMedia></CurrentMedia></Data>')
328 self
.failUnless((self
.client
.state
== 'GOTPERSONAL'), 'Failed to detect new personal message')
329 self
.client
.dataReceived('UBX foo@bar.com 0\r\n')
330 self
.failUnless((self
.client
.state
== 'PERSONALGONE'), 'Failed to detect personal message disappearing')
333 def testAsyncPhoneChange(self
):
334 c
= msn
.MSNContact(userHandle
='userHandle@email.com')
335 self
.client
.factory
.contacts
= msn
.MSNContactList()
336 self
.client
.factory
.contacts
.addContact(c
)
337 self
.client
.makeConnection(StringIOWithoutClosing())
338 self
.client
.lineReceived("BPR 101 userHandle@email.com PHH 123%20456")
339 c
= self
.client
.factory
.contacts
.getContact('userHandle@email.com')
340 self
.failUnless(self
.client
.state
== 'GOTPHONE', "Did not fire phone change callback")
341 self
.failUnless(c
.homePhone
== '123 456', "Did not update the contact's phone number")
342 self
.failUnless(self
.client
.factory
.contacts
.version
== 101, "Did not update list version")
344 def testLateBPR(self
):
346 This test makes sure that if a BPR response that was meant
347 to be part of a SYN response (but came after the last LST)
348 is received, the correct contact is updated and all is well
350 self
.client
.makeConnection(StringIOWithoutClosing())
351 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
353 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self
.client
.currentID
,
356 "LSG Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
357 "LST N=userHandle@email.com F=Some%20Name C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 13 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
360 map(self
.client
.lineReceived
, lines
)
361 contact
= self
.client
.factory
.contacts
.getContact('userHandle@email.com')
362 self
.failUnless(contact
.homePhone
== '123 456', "Did not update contact's phone number")
365 def testUserRemovedMe(self
):
366 self
.client
.factory
.contacts
= msn
.MSNContactList()
367 contact
= msn
.MSNContact(userHandle
='foo@foo.com')
368 contact
.addToList(msn
.REVERSE_LIST
)
369 self
.client
.factory
.contacts
.addContact(contact
)
370 self
.client
.lineReceived("REM 0 RL foo@foo.com")
371 self
.failUnless(self
.client
.state
== 'USERREMOVEDME', "Failed to remove user from reverse list")
373 def testUserAddedMe(self
):
374 self
.client
.factory
.contacts
= msn
.MSNContactList()
375 self
.client
.lineReceived("ADC 0 RL N=foo@foo.com F=Screen%20Name")
376 self
.failUnless(self
.client
.state
== 'USERADDEDME', "Failed to add user to reverse lise")
378 def testAsyncSwitchboardInvitation(self
):
379 self
.client
.lineReceived("RNG 1234 192.168.1.1:1863 CKI 123.456 foo@foo.com Screen%20Name")
380 self
.failUnless((self
.client
.state
== 'SBINVITED'), 'Failed to detect switchboard invitation')
383 #######################################
384 # Notification with fake server tests #
385 #######################################
387 class FakeNotificationServer(msn
.MSNEventBase
):
388 def handle_CHG(self
, params
):
391 self
.sendLine("CHG %s %s %s %s" % (params
[0], params
[1], params
[2], params
[3]))
393 def handle_BLP(self
, params
):
394 self
.sendLine("BLP %s %s 100" % (params
[0], params
[1]))
396 def handle_ADC(self
, params
):
398 list = msn
.listCodeToID
[params
[1].lower()]
399 if list == msn
.FORWARD_LIST
:
413 if userHandle
and userGuid
:
414 self
.transport
.loseConnection()
418 self
.transport
.loseConnection()
420 self
.sendLine("ADC %s FL N=%s F=%s C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx %s" % (trid
, userHandle
, screenName
, groups
))
423 raise "NotImplementedError"
426 self
.transport
.loseConnection()
427 if not params
[2].startswith("N=") and params
[2].count('@') == 1:
428 self
.transport
.loseConnection()
429 self
.sendLine("ADC %s %s %s" % (params
[0], params
[1], params
[2]))
431 def handle_REM(self
, params
):
433 self
.transport
.loseConnection()
436 trid
= int(params
[0])
437 listType
= msn
.listCodeToID
[params
[1].lower()]
439 self
.transport
.loseConnection()
440 if listType
== msn
.FORWARD_LIST
and params
[2].count('@') > 0:
441 self
.transport
.loseConnection()
442 elif listType
!= msn
.FORWARD_LIST
and params
[2].count('@') != 1:
443 self
.transport
.loseConnection()
445 self
.sendLine("REM %s %s %s" % (params
[0], params
[1], params
[2]))
447 def handle_PRP(self
, params
):
449 self
.transport
.loseConnection()
450 if params
[1] == "MFN":
451 self
.sendLine("PRP %s MFN %s" % (params
[0], params
[2]))
453 # Only friendly names are implemented
454 self
.transport
.loseConnection()
456 def handle_UUX(self
, params
):
458 self
.transport
.loseConnection()
462 self
.currentMessage
= msn
.MSNMessage(length
=l
, userHandle
=params
[0], screenName
="UUX", specialMessage
=True)
465 self
.sendLine("UUX %s 0" % params
[0])
467 def checkMessage(self
, message
):
468 if message
.specialMessage
:
469 if message
.screenName
== "UUX":
470 self
.sendLine("UUX %s 0" % message
.userHandle
)
474 def handle_XFR(self
, params
):
476 self
.transport
.loseConnection()
478 if params
[1] != "SB":
479 self
.transport
.loseConnection()
481 self
.sendLine("XFR %s SB 129.129.129.129:1234 CKI SomeSecret" % params
[0])
485 class FakeNotificationClient(msn
.NotificationClient
):
486 def doStatusChange(self
):
487 def testcb((status
,)):
488 if status
== msn
.STATUS_AWAY
:
490 self
.transport
.loseConnection()
491 d
= self
.changeStatus(msn
.STATUS_AWAY
)
492 d
.addCallback(testcb
)
494 def doPrivacyMode(self
):
496 if priv
.upper() == 'AL':
498 self
.transport
.loseConnection()
499 d
= self
.setPrivacyMode(True)
500 d
.addCallback(testcb
)
502 def doAddContactFL(self
):
503 def testcb((listType
, userGuid
, userHandle
, screenName
)):
504 if listType
& msn
.FORWARD_LIST
and \
505 userGuid
== "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" and \
506 userHandle
== "foo@bar.com" and \
507 screenName
== "foo@bar.com" and \
508 self
.factory
.contacts
.getContact(userHandle
):
510 self
.transport
.loseConnection()
511 d
= self
.addContact(msn
.FORWARD_LIST
, "foo@bar.com")
512 d
.addCallback(testcb
)
514 def doAddContactAL(self
):
515 def testcb((listType
, userGuid
, userHandle
, screenName
)):
516 if listType
& msn
.ALLOW_LIST
and \
517 userHandle
== "foo@bar.com" and \
518 not userGuid
and not screenName
and \
519 self
.factory
.contacts
.getContact(userHandle
):
521 self
.transport
.loseConnection()
522 d
= self
.addContact(msn
.ALLOW_LIST
, "foo@bar.com")
523 d
.addCallback(testcb
)
525 def doRemContactFL(self
):
526 def testcb((listType
, userHandle
, groupID
)):
527 if listType
& msn
.FORWARD_LIST
and \
528 userHandle
== "foo@bar.com":
530 self
.transport
.loseConnection()
531 d
= self
.remContact(msn
.FORWARD_LIST
, "foo@bar.com")
532 d
.addCallback(testcb
)
534 def doRemContactAL(self
):
535 def testcb((listType
, userHandle
, groupID
)):
536 if listType
& msn
.ALLOW_LIST
and \
537 userHandle
== "foo@bar.com":
539 self
.transport
.loseConnection()
540 d
= self
.remContact(msn
.ALLOW_LIST
, "foo@bar.com")
541 d
.addCallback(testcb
)
543 def doScreenNameChange(self
):
546 self
.transport
.loseConnection()
547 d
= self
.changeScreenName("Some new name")
548 d
.addCallback(testcb
)
550 def doPersonalChange(self
, personal
):
551 def testcb((checkPersonal
,)):
552 if checkPersonal
== personal
:
554 self
.transport
.loseConnection()
555 d
= self
.changePersonalMessage(personal
)
556 d
.addCallback(testcb
)
558 def doAvatarChange(self
, data
):
561 self
.transport
.loseConnection()
562 d
= self
.changeAvatar(data
, True)
563 d
.addCallback(testcb
)
565 def doRequestSwitchboard(self
):
566 def testcb((host
, port
, key
)):
567 if host
== "129.129.129.129" and port
== 1234 and key
== "SomeSecret":
569 self
.transport
.loseConnection()
570 d
= self
.requestSwitchboardServer()
571 d
.addCallback(testcb
)
573 class FakeServerNotificationTests(unittest
.TestCase
):
574 """ tests the NotificationClient against a fake server. """
577 self
.client
= FakeNotificationClient()
578 self
.client
.factory
= msn
.NotificationFactory()
579 self
.client
.test
= 'FAIL'
580 self
.server
= FakeNotificationServer()
581 self
.loop
= LoopbackCon(self
.client
, self
.server
)
584 self
.loop
.disconnect()
586 def testChangeStatus(self
):
587 self
.client
.doStatusChange()
588 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
589 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change status properly')
591 def testSetPrivacyMode(self
):
592 self
.client
.factory
.contacts
= msn
.MSNContactList()
593 self
.client
.doPrivacyMode()
594 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
595 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change privacy mode')
597 def testSyncList(self
):
598 self
.client
.doSyncList()
599 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
600 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to synchronise list')
601 testSyncList
.skip
= "Will do after list versions."
603 def testAddContactFL(self
):
604 self
.client
.factory
.contacts
= msn
.MSNContactList()
605 self
.client
.doAddContactFL()
606 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
607 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to add contact to forward list')
609 def testAddContactAL(self
):
610 self
.client
.factory
.contacts
= msn
.MSNContactList()
611 self
.client
.doAddContactAL()
612 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
613 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to add contact to allow list')
615 def testRemContactFL(self
):
616 self
.client
.factory
.contacts
= msn
.MSNContactList()
617 self
.client
.factory
.contacts
.addContact(msn
.MSNContact(userGuid
="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", userHandle
="foo@bar.com", screenName
="Some guy", lists
=msn
.FORWARD_LIST
))
618 self
.client
.doRemContactFL()
619 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
620 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to remove contact from forward list')
622 def testRemContactAL(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
.ALLOW_LIST
))
625 self
.client
.doRemContactAL()
626 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
627 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to remove contact from allow list')
629 def testChangedScreenName(self
):
630 self
.client
.doScreenNameChange()
631 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
632 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change screen name properly')
634 def testChangePersonal1(self
):
635 self
.client
.doPersonalChange("Some personal message")
636 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
637 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change personal message properly')
639 def testChangePersonal2(self
):
640 self
.client
.doPersonalChange("")
641 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
642 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change personal message properly')
644 def testChangeAvatar(self
):
645 self
.client
.doAvatarChange("DATADATADATADATA")
646 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
647 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change avatar properly')
649 def testRequestSwitchboard(self
):
650 self
.client
.doRequestSwitchboard()
651 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
652 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to request switchboard')
655 #################################
656 # Notification challenges tests #
657 #################################
659 class DummyChallengeNotificationServer(msn
.MSNEventBase
):
660 def doChallenge(self
, challenge
, response
):
662 self
.response
= response
663 self
.sendLine("CHL 0 " + challenge
)
665 def checkMessage(self
, message
):
666 if message
.message
== self
.response
:
668 self
.transport
.loseConnection()
671 def handle_QRY(self
, params
):
673 if len(params
) == 3 and params
[1] == "PROD0090YUAUV{2B" and params
[2] == "32":
675 self
.currentMessage
= msn
.MSNMessage(length
=32, userHandle
="QRY", screenName
="QRY", specialMessage
=True)
678 self
.transport
.loseConnection()
680 class DummyChallengeNotificationClient(msn
.NotificationClient
):
681 def connectionMade(self
):
682 msn
.MSNEventBase
.connectionMade(self
)
684 def handle_CHL(self
, params
):
685 msn
.NotificationClient
.handle_CHL(self
, params
)
686 self
.transport
.loseConnection()
689 class NotificationChallengeTests(unittest
.TestCase
):
690 """ tests the responses to the CHLs the server sends """
693 self
.client
= DummyChallengeNotificationClient()
694 self
.server
= DummyChallengeNotificationServer()
695 self
.loop
= LoopbackCon(self
.client
, self
.server
)
698 self
.loop
.disconnect()
700 def testChallenges(self
):
701 challenges
= [('13038318816579321232', 'b01c13020e374d4fa20abfad6981b7a9'),
702 ('23055170411503520698', 'ae906c3f2946d25e7da1b08b0b247659'),
703 ('37819769320541083311', 'db79d37dadd9031bef996893321da480'),
704 ('93662730714769834295', 'd619dfbb1414004d34d0628766636568'),
705 ('31154116582196216093', '95e96c4f8cfdba6f065c8869b5e984e9')]
706 for challenge
, response
in challenges
:
707 self
.server
.doChallenge(challenge
, response
)
708 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
709 self
.failUnless((self
.server
.state
== 'PASS'), 'Incorrect challenge response.')
712 ###########################
713 # Notification ping tests #
714 ###########################
716 class DummyPingNotificationServer(LineReceiver
):
717 def lineReceived(self
, line
):
718 if line
.startswith("PNG") and self
.good
:
719 self
.sendLine("QNG 50")
721 class DummyPingNotificationClient(msn
.NotificationClient
):
722 def connectionMade(self
):
723 self
.pingCheckerStart()
725 def sendLine(self
, line
):
726 msn
.NotificationClient
.sendLine(self
, line
)
729 self
.transport
.loseConnection() # But not for real, just to end the test
731 def connectionLost(self
, reason
):
733 self
.state
= 'DISCONNECTED'
735 class NotificationPingTests(unittest
.TestCase
):
736 """ tests pinging in the NotificationClient class """
740 self
.client
= DummyPingNotificationClient()
741 self
.server
= DummyPingNotificationServer()
742 self
.client
.state
= 'CONNECTED'
743 self
.client
.count
= 0
744 self
.loop
= LoopbackCon(self
.client
, self
.server
)
749 self
.loop
.disconnect()
751 def testPingGood(self
):
752 self
.server
.good
= True
753 self
.loop
.doSteps(100)
754 self
.failUnless((self
.client
.state
== 'CONNECTED'), 'Should be connected.')
756 def testPingBad(self
):
757 self
.server
.good
= False
758 self
.loop
.doSteps(100)
759 self
.failUnless((self
.client
.state
== 'DISCONNECTED'), 'Should be disconnected.')
764 ###########################
765 # Switchboard basic tests #
766 ###########################
768 class DummySwitchboardServer(msn
.MSNEventBase
):
769 def handle_USR(self
, params
):
771 self
.transport
.loseConnection()
772 if params
[1] == 'foo@bar.com' and params
[2] == 'somekey':
773 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
775 def handle_ANS(self
, params
):
777 self
.transport
.loseConnection()
778 if params
[1] == 'foo@bar.com' and params
[2] == 'somekey' and params
[3] == 'someSID':
779 self
.sendLine("ANS %s OK" % params
[0])
781 def handle_CAL(self
, params
):
783 self
.transport
.loseConnection()
784 if params
[1] == 'friend@hotmail.com':
785 self
.sendLine("CAL %s RINGING 1111122" % params
[0])
787 self
.transport
.loseConnection()
789 def checkMessage(self
, message
):
790 if message
.message
== 'Hi how are you today?':
791 self
.sendLine("ACK " + message
.userHandle
) # Relies on TRID getting stored in userHandle trick
793 self
.transport
.loseConnection()
796 class DummySwitchboardClient(msn
.SwitchboardClient
):
798 self
.state
= 'LOGGEDIN'
799 self
.transport
.loseConnection()
801 def gotChattingUsers(self
, users
):
802 if users
== {'fred@hotmail.com': 'fred', 'jack@email.com': 'jack has a nickname!'}:
803 self
.state
= 'GOTCHATTINGUSERS'
805 def userJoined(self
, userHandle
, screenName
):
806 if userHandle
== "friend@hotmail.com" and screenName
== "friend nickname":
807 self
.state
= 'USERJOINED'
809 def userLeft(self
, userHandle
):
810 if userHandle
== "friend@hotmail.com":
811 self
.state
= 'USERLEFT'
813 def gotContactTyping(self
, message
):
814 if message
.userHandle
== 'foo@bar.com':
815 self
.state
= 'USERTYPING'
817 def gotMessage(self
, message
):
818 if message
.userHandle
== 'friend@hotmail.com' and \
819 message
.screenName
== 'Friend Nickname' and \
820 message
.message
== 'Hello.':
821 self
.state
= 'GOTMESSAGE'
823 def doSendInvite(self
):
826 self
.state
= 'INVITESUCCESS'
827 self
.transport
.loseConnection()
828 d
= self
.inviteUser('friend@hotmail.com')
829 d
.addCallback(testcb
)
831 def doSendMessage(self
):
833 self
.state
= 'MESSAGESUCCESS'
834 self
.transport
.loseConnection()
836 m
.setHeader("Content-Type", "text/plain; charset=UTF-8")
837 m
.message
= 'Hi how are you today?'
838 m
.ack
= msn
.MSNMessage
.MESSAGE_ACK
839 d
= self
.sendMessage(m
)
840 d
.addCallback(testcb
)
843 class SwitchboardBasicTests(unittest
.TestCase
):
844 """ Tests basic functionality of switchboard sessions """
846 self
.client
= DummySwitchboardClient()
847 self
.client
.state
= 'START'
848 self
.client
.userHandle
= 'foo@bar.com'
849 self
.client
.key
= 'somekey'
850 self
.client
.sessionID
= 'someSID'
851 self
.server
= DummySwitchboardServer()
852 self
.loop
= LoopbackCon(self
.client
, self
.server
)
855 self
.loop
.disconnect()
857 def _testSB(self
, reply
):
858 self
.client
.reply
= reply
859 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
860 self
.failUnless((self
.client
.state
== 'LOGGEDIN'), 'Failed to login with reply='+str(reply
))
868 def testChattingUsers(self
):
869 lines
= ["IRO 1 1 2 fred@hotmail.com fred",
870 "IRO 1 2 2 jack@email.com jack%20has%20a%20nickname%21"]
872 self
.client
.lineReceived(line
)
873 self
.failUnless((self
.client
.state
== 'GOTCHATTINGUSERS'), 'Failed to get chatting users')
875 def testUserJoined(self
):
876 self
.client
.lineReceived("JOI friend@hotmail.com friend%20nickname")
877 self
.failUnless((self
.client
.state
== 'USERJOINED'), 'Failed to notice user joining')
879 def testUserLeft(self
):
880 self
.client
.lineReceived("BYE friend@hotmail.com")
881 self
.failUnless((self
.client
.state
== 'USERLEFT'), 'Failed to notice user leaving')
883 def testTypingCheck(self
):
884 m
= 'MSG foo@bar.com Foo 80\r\n'
885 m
+= 'MIME-Version: 1.0\r\n'
886 m
+= 'Content-Type: text/x-msmsgscontrol\r\n'
887 m
+= 'TypingUser: foo@bar\r\n'
889 self
.client
.dataReceived(m
)
890 self
.failUnless((self
.client
.state
== 'USERTYPING'), 'Failed to detect typing notification')
892 def testGotMessage(self
):
893 m
= 'MSG friend@hotmail.com Friend%20Nickname 68\r\n'
894 m
+= 'MIME-Version: 1.0\r\n'
895 m
+= 'Content-Type: text/plain; charset=UTF-8\r\n'
897 self
.client
.dataReceived(m
)
898 self
.failUnless((self
.client
.state
== 'GOTMESSAGE'), 'Failed to detect message')
900 def testInviteUser(self
):
901 self
.client
.connectionMade
= lambda: None
902 self
.client
.doSendInvite()
903 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
904 self
.failUnless((self
.client
.state
== 'INVITESUCCESS'), 'Failed to invite user')
906 def testSendMessage(self
):
907 self
.client
.connectionMade
= lambda: None
908 self
.client
.doSendMessage()
909 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
910 self
.failUnless((self
.client
.state
== 'MESSAGESUCCESS'), 'Failed to send message')
917 class DummySwitchboardP2PServerHelper(msn
.MSNEventBase
):
918 def __init__(self
, server
):
919 msn
.MSNEventBase
.__init
__(self
)
922 def handle_USR(self
, params
):
924 self
.transport
.loseConnection()
925 self
.userHandle
= params
[1]
926 if params
[1] == 'foo1@bar.com' and params
[2] == 'somekey1':
927 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
928 if params
[1] == 'foo2@bar.com' and params
[2] == 'somekey2':
929 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
931 def checkMessage(self
, message
):
934 def gotMessage(self
, message
):
935 message
.userHandle
= self
.userHandle
936 message
.screenName
= self
.userHandle
937 self
.server
.gotMessage(message
, self
)
939 def sendMessage(self
, message
):
940 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
941 self
.sendLine("MSG %s %s %s" % (message
.userHandle
, message
.screenName
, message
.length
))
942 self
.sendLine('MIME-Version: %s' % message
.getHeader('MIME-Version'))
943 self
.sendLine('Content-Type: %s' % message
.getHeader('Content-Type'))
944 for header
in [h
for h
in message
.headers
.items() if h
[0].lower() not in ('mime-version','content-type')]:
945 self
.sendLine("%s: %s" % (header
[0], header
[1]))
946 self
.transport
.write("\r\n")
947 self
.transport
.write(message
.message
)
950 class DummySwitchboardP2PServer
:
955 c
= DummySwitchboardP2PServerHelper(self
)
956 self
.clients
.append(c
)
959 def gotMessage(self
, message
, sender
):
960 for c
in self
.clients
:
962 c
.sendMessage(message
)
964 class DummySwitchboardP2PClient(msn
.SwitchboardClient
):
965 def gotMessage(self
, message
):
966 if message
.message
== "Test Message" and message
.userHandle
== "foo1@bar.com":
967 self
.status
= "GOTMESSAGE"
969 def gotFileReceive(self
, fileReceive
):
970 self
.fileReceive
= fileReceive
972 class SwitchboardP2PTests(unittest
.TestCase
):
974 self
.server
= DummySwitchboardP2PServer()
975 self
.client1
= DummySwitchboardP2PClient()
976 self
.client1
.key
= 'somekey1'
977 self
.client1
.userHandle
= 'foo1@bar.com'
978 self
.client2
= DummySwitchboardP2PClient()
979 self
.client2
.key
= 'somekey2'
980 self
.client2
.userHandle
= 'foo2@bar.com'
981 self
.client2
.status
= "INIT"
982 self
.loop1
= LoopbackCon(self
.client1
, self
.server
.newClient())
983 self
.loop2
= LoopbackCon(self
.client2
, self
.server
.newClient())
986 self
.loop1
.disconnect()
987 self
.loop2
.disconnect()
989 def _loop(self
, steps
=1):
990 for i
in xrange(steps
):
991 self
.loop1
.doSteps(1)
992 self
.loop2
.doSteps(1)
994 def testMessage(self
):
995 self
.client1
.sendMessage(msn
.MSNMessage(message
='Test Message'))
997 self
.failUnless((self
.client2
.status
== "GOTMESSAGE"), "Fake switchboard server not working.")
999 def _generateData(self
):
1001 for i
in xrange(3000):
1002 data
+= struct
.pack("<L", random
.randint(0, sys
.maxint
))
1005 def testAvatars(self
):
1006 self
.gotAvatar
= False
1008 # Set up the avatar for client1
1009 imageData
= self
._generateData
()
1010 self
.client1
.msnobj
= msn
.MSNObject()
1011 self
.client1
.msnobj
.setData('foo1@bar.com', imageData
)
1012 self
.client1
.msnobj
.makeText()
1014 # Make client2 request the avatar
1015 def avatarCallback((data
,)):
1016 self
.gotAvatar
= (data
== imageData
)
1017 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', msnobj
=self
.client1
.msnobj
)
1018 d
= self
.client2
.sendAvatarRequest(msnContact
)
1019 d
.addCallback(avatarCallback
)
1021 # Let them do their thing
1024 # Check that client2 got the avatar
1025 self
.failUnless((self
.gotAvatar
), "Failed to transfer avatar")
1027 def testFilesHappyPath(self
):
1028 fileData
= self
._generateData
()
1029 self
.gotFile
= False
1031 # Send the file (client2->client1)
1032 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1033 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1034 def accepted((yes
,)):
1036 fileSend
.write(fileData
)
1039 raise "TransferDeclined"
1041 raise "TransferError"
1042 d
.addCallback(accepted
)
1043 d
.addErrback(failed
)
1045 # Let the request get pushed to client1
1050 self
.gotFile
= (data
== fileData
)
1051 fileBuffer
= msn
.StringBuffer(finished
)
1052 fileReceive
= self
.client1
.fileReceive
1053 self
.failUnless((fileReceive
.filename
== "myfile.txt" and fileReceive
.filesize
== len(fileData
)), "Filename or length wrong.")
1054 fileReceive
.accept(fileBuffer
)
1059 self
.failUnless((self
.gotFile
), "Failed to transfer file")
1061 def testFilesHappyChunkedPath(self
):
1062 fileData
= self
._generateData
()
1063 self
.gotFile
= False
1065 # Send the file (client2->client1)
1066 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1067 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1068 def accepted((yes
,)):
1070 fileSend
.write(fileData
[:len(fileData
)/2])
1071 fileSend
.write(fileData
[len(fileData
)/2:])
1074 raise "TransferDeclined"
1076 raise "TransferError"
1077 d
.addCallback(accepted
)
1078 d
.addErrback(failed
)
1080 # Let the request get pushed to client1
1085 self
.gotFile
= (data
== fileData
)
1086 fileBuffer
= msn
.StringBuffer(finished
)
1087 fileReceive
= self
.client1
.fileReceive
1088 self
.failUnless((fileReceive
.filename
== "myfile.txt" and fileReceive
.filesize
== len(fileData
)), "Filename or length wrong.")
1089 fileReceive
.accept(fileBuffer
)
1094 self
.failUnless((self
.gotFile
), "Failed to transfer file")
1096 def testFilesDeclinePath(self
):
1097 fileData
= self
._generateData
()
1098 self
.gotDecline
= False
1100 # Send the file (client2->client1)
1101 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1102 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1103 def accepted((yes
,)):
1104 self
.failUnless((not yes
), "Failed to understand a decline.")
1105 self
.gotDecline
= True
1107 raise "TransferError"
1108 d
.addCallback(accepted
)
1109 d
.addErrback(failed
)
1111 # Let the request get pushed to client1
1115 fileReceive
= self
.client1
.fileReceive
1116 fileReceive
.reject()
1118 # Let the decline get pushed to client2
1121 self
.failUnless((self
.gotDecline
), "Failed to understand a decline, ignored.")
1128 class FileTransferTestCase(unittest
.TestCase
):
1129 """ test FileSend against FileReceive """
1130 skip
= "Not implemented"
1133 self
.input = StringIOWithoutClosing()
1134 self
.input.writelines(['a'] * 7000)
1136 self
.output
= StringIOWithoutClosing()
1142 def testFileTransfer(self
):
1144 sender
= msnft
.MSNFTP_FileSend(self
.input)
1146 sender
.fileSize
= 7000
1147 client
= msnft
.MSNFTP_FileReceive(auth
, "foo@bar.com", self
.output
)
1148 client
.fileSize
= 7000
1149 loop
= LoopbackCon(client
, sender
)
1151 self
.failUnless((client
.completed
and sender
.completed
), "send failed to complete")
1152 self
.failUnless((self
.input.getvalue() == self
.output
.getvalue()), "saved file does not match original")