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
27 class StringIOWithoutClosing(StringIO
.StringIO
):
30 def loseConnection(self
): pass
33 def __init__(self
, con1
, con2
):
36 self
.con1ToCon2
= loopback
.LoopbackRelay(con1
)
37 self
.con2ToCon1
= loopback
.LoopbackRelay(con2
)
38 con2
.makeConnection(self
.con1ToCon2
)
39 con1
.makeConnection(self
.con2ToCon1
)
42 def doSteps(self
, steps
=1):
43 """ Returns true if the connection finished """
47 self
.con1ToCon2
.clearBuffer()
48 self
.con2ToCon1
.clearBuffer()
49 if self
.con1ToCon2
.shouldLose
:
50 self
.con1ToCon2
.clearBuffer()
53 elif self
.con2ToCon1
.shouldLose
:
65 self
.con1
.connectionLost(failure
.Failure(main
.CONNECTION_DONE
))
66 self
.con2
.connectionLost(failure
.Failure(main
.CONNECTION_DONE
))
76 class PassportTests(unittest
.TestCase
):
80 self
.deferred
= Deferred()
81 self
.deferred
.addCallback(lambda r
: self
.result
.append(r
))
82 self
.deferred
.addErrback(printError
)
85 protocol
= msn
.PassportNexus(self
.deferred
, 'https://foobar.com/somepage.quux')
87 'Content-Length' : '0',
88 'Content-Type' : 'text/html',
89 'PassportURLs' : 'DARealm=Passport.Net,DALogin=login.myserver.com/,DAReg=reg.myserver.com'
91 transport
= StringIOWithoutClosing()
92 protocol
.makeConnection(transport
)
93 protocol
.dataReceived('HTTP/1.0 200 OK\r\n')
94 for (h
,v
) in headers
.items(): protocol
.dataReceived('%s: %s\r\n' % (h
,v
))
95 protocol
.dataReceived('\r\n')
96 self
.failUnless(self
.result
[0] == "https://login.myserver.com/")
98 def _doLoginTest(self
, response
, headers
):
99 protocol
= msn
.PassportLogin(self
.deferred
,'foo@foo.com','testpass','https://foo.com/', 'a')
100 protocol
.makeConnection(StringIOWithoutClosing())
101 protocol
.dataReceived(response
)
102 for (h
,v
) in headers
.items(): protocol
.dataReceived('%s: %s\r\n' % (h
,v
))
103 protocol
.dataReceived('\r\n')
105 def testPassportLoginSuccess(self
):
107 'Content-Length' : '0',
108 'Content-Type' : 'text/html',
109 'Authentication-Info' : "Passport1.4 da-status=success,tname=MSPAuth," +
110 "tname=MSPProf,tname=MSPSec,from-PP='somekey'," +
111 "ru=http://messenger.msn.com"
113 self
._doLoginTest
('HTTP/1.1 200 OK\r\n', headers
)
114 self
.failUnless(self
.result
[0] == (msn
.LOGIN_SUCCESS
, 'somekey'))
116 def testPassportLoginFailure(self
):
118 'Content-Type' : 'text/html',
119 'WWW-Authenticate' : 'Passport1.4 da-status=failed,' +
120 'srealm=Passport.NET,ts=-3,prompt,cburl=http://host.com,' +
121 'cbtxt=the%20error%20message'
123 self
._doLoginTest
('HTTP/1.1 401 Unauthorized\r\n', headers
)
124 self
.failUnless(self
.result
[0] == (msn
.LOGIN_FAILURE
, 'the error message'))
126 def testPassportLoginRedirect(self
):
128 'Content-Type' : 'text/html',
129 'Authentication-Info' : 'Passport1.4 da-status=redir',
130 'Location' : 'https://newlogin.host.com/'
132 self
._doLoginTest
('HTTP/1.1 302 Found\r\n', headers
)
133 self
.failUnless(self
.result
[0] == (msn
.LOGIN_REDIRECT
, 'https://newlogin.host.com/', 'a'))
137 ######################
138 # Notification tests #
139 ######################
141 class DummyNotificationClient(msn
.NotificationClient
):
142 def loggedIn(self
, userHandle
, verified
):
143 if userHandle
== 'foo@bar.com' and verified
:
146 def gotProfile(self
, message
):
147 self
.state
= 'PROFILE'
149 def gotContactStatus(self
, userHandle
, code
, screenName
):
150 if code
== msn
.STATUS_AWAY
and userHandle
== "foo@bar.com" and screenName
== "Test Screen Name":
151 c
= self
.factory
.contacts
.getContact(userHandle
)
152 if c
.caps
& msn
.MSNContact
.MSNC1
and c
.msnobj
:
153 self
.state
= 'INITSTATUS'
155 def contactStatusChanged(self
, userHandle
, code
, screenName
):
156 if code
== msn
.STATUS_LUNCH
and userHandle
== "foo@bar.com" and screenName
== "Test Name":
157 self
.state
= 'NEWSTATUS'
159 def contactAvatarChanged(self
, userHandle
, hash):
160 if userHandle
== "foo@bar.com" and hash == "trC8SlFx2sWQxZMIBAWSEnXc8oQ=":
161 self
.state
= 'NEWAVATAR'
162 elif self
.state
== 'NEWAVATAR' and hash == "":
163 self
.state
= 'AVATARGONE'
165 def contactPersonalChanged(self
, userHandle
, personal
):
166 if userHandle
== 'foo@bar.com' and personal
== 'My Personal Message':
167 self
.state
= 'GOTPERSONAL'
168 elif userHandle
== 'foo@bar.com' and personal
== '':
169 self
.state
= 'PERSONALGONE'
171 def contactOffline(self
, userHandle
):
172 if userHandle
== "foo@bar.com": self
.state
= 'OFFLINE'
174 def statusChanged(self
, code
):
175 if code
== msn
.STATUS_HIDDEN
: self
.state
= 'MYSTATUS'
177 def listSynchronized(self
, *args
):
178 self
.state
= 'GOTLIST'
180 def gotPhoneNumber(self
, userHandle
, phoneType
, number
):
181 self
.state
= 'GOTPHONE'
183 def userRemovedMe(self
, userHandle
):
184 c
= self
.factory
.contacts
.getContact(userHandle
)
185 if not c
: self
.state
= 'USERREMOVEDME'
187 def userAddedMe(self
, userGuid
, userHandle
, screenName
):
188 c
= self
.factory
.contacts
.getContact(userHandle
)
189 if c
and (c
.lists | msn
.PENDING_LIST
) and (screenName
== 'Screen Name'):
190 self
.state
= 'USERADDEDME'
192 def gotSwitchboardInvitation(self
, sessionID
, host
, port
, key
, userHandle
, screenName
):
193 if sessionID
== 1234 and \
194 host
== '192.168.1.1' and \
196 key
== '123.456' and \
197 userHandle
== 'foo@foo.com' and \
198 screenName
== 'Screen Name':
199 self
.state
= 'SBINVITED'
201 def gotMSNAlert(self
, body
, action
, subscr
):
202 self
.state
= 'NOTIFICATION'
204 def gotInitialEmailNotification(self
, inboxunread
, foldersunread
):
205 if inboxunread
== 1 and foldersunread
== 0:
206 self
.state
= 'INITEMAIL1'
208 self
.state
= 'INITEMAIL2'
210 def gotRealtimeEmailNotification(self
, mailfrom
, fromaddr
, subject
):
211 if mailfrom
== 'Some Person' and fromaddr
== 'example@passport.com' and subject
== 'newsubject':
212 self
.state
= 'REALTIMEEMAIL'
214 class NotificationTests(unittest
.TestCase
):
215 """ testing the various events in NotificationClient """
218 self
.client
= DummyNotificationClient()
219 self
.client
.factory
= msn
.NotificationFactory()
220 self
.client
.state
= 'START'
226 self
.client
.lineReceived('USR 1 OK foo@bar.com 1')
227 self
.failUnless((self
.client
.state
== 'LOGIN'), 'Failed to detect successful login')
229 def testProfile(self
):
230 m
= 'MSG Hotmail Hotmail 353\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsprofile; charset=UTF-8\r\n'
231 m
+= 'LoginTime: 1016941010\r\nEmailEnabled: 1\r\nMemberIdHigh: 40000\r\nMemberIdLow: -600000000\r\nlang_preference: 1033\r\n'
232 m
+= 'preferredEmail: foo@bar.com\r\ncountry: AU\r\nPostalCode: 90210\r\nGender: M\r\nKid: 0\r\nAge:\r\nsid: 400\r\n'
233 m
+= 'kv: 2\r\nMSPAuth: 2CACCBCCADMoV8ORoz64BVwmjtksIg!kmR!Rj5tBBqEaW9hc4YnPHSOQ$$\r\n\r\n'
234 self
.client
.dataReceived(m
)
235 self
.failUnless((self
.client
.state
== 'PROFILE'), 'Failed to detect initial profile')
237 def testInitialEmailNotification(self
):
238 m
= 'MIME-Version: 1.0\r\nContent-Type: text/x-msmsgsinitialemailnotification; charset=UTF-8\r\n'
239 m
+= '\r\nInbox-Unread: 1\r\nFolders-Unread: 0\r\nInbox-URL: /cgi-bin/HoTMaiL\r\n'
240 m
+= 'Folders-URL: /cgi-bin/folders\r\nPost-URL: http://www.hotmail.com\r\n\r\n'
241 m
= 'MSG Hotmail Hotmail %s\r\n' % (str(len(m
))) + m
242 self
.client
.dataReceived(m
)
243 self
.failUnless((self
.client
.state
== 'INITEMAIL1'), 'Failed to detect initial email notification')
245 def testNoInitialEmailNotification(self
):
246 m
= 'MIME-Version: 1.0\r\nContent-Type: text/x-msmsgsinitialemailnotification; charset=UTF-8\r\n'
247 m
+= '\r\nInbox-Unread: 0\r\nFolders-Unread: 0\r\nInbox-URL: /cgi-bin/HoTMaiL\r\n'
248 m
+= 'Folders-URL: /cgi-bin/folders\r\nPost-URL: http://www.hotmail.com\r\n\r\n'
249 m
= 'MSG Hotmail Hotmail %s\r\n' % (str(len(m
))) + m
250 self
.client
.dataReceived(m
)
251 self
.failUnless((self
.client
.state
!= 'INITEMAIL2'), 'Detected initial email notification when I should not have')
253 def testRealtimeEmailNotification(self
):
254 m
= 'MSG Hotmail Hotmail 356\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsemailnotification; charset=UTF-8\r\n'
255 m
+= '\r\nFrom: Some Person\r\nMessage-URL: /cgi-bin/getmsg?msg=MSG1050451140.21&start=2310&len=2059&curmbox=ACTIVE\r\n'
256 m
+= 'Post-URL: https://loginnet.passport.com/ppsecure/md5auth.srf?lc=1038\r\n'
257 m
+= 'Subject: =?"us-ascii"?Q?newsubject?=\r\nDest-Folder: ACTIVE\r\nFrom-Addr: example@passport.com\r\nid: 2\r\n'
258 self
.client
.dataReceived(m
)
259 self
.failUnless((self
.client
.state
== 'REALTIMEEMAIL'), 'Failed to detect realtime email notification')
261 def testMSNAlert(self
):
262 m
= '<NOTIFICATION ver="2" id="1342902633" siteid="199999999" siteurl="http://alerts.msn.com">\r\n'
263 m
+= '<TO pid="0x0006BFFD:0x8582C0FB" name="example@passport.com"/>\r\n'
264 m
+= '<MSG pri="1" id="1342902633">\r\n'
265 m
+= '<SUBSCR url="http://g.msn.com/3ALMSNTRACKING/199999999ToastChange?http://alerts.msn.com/Alerts/MyAlerts.aspx?strela=1"/>\r\n'
266 m
+= '<ACTION url="http://g.msn.com/3ALMSNTRACKING/199999999ToastAction?http://alerts.msn.com/Alerts/MyAlerts.aspx?strela=1"/>\r\n'
267 m
+= '<BODY lang="3076" icon="">\r\n'
268 m
+= '<TEXT>utf8-encoded text</TEXT></BODY></MSG>\r\n'
269 m
+= '</NOTIFICATION>\r\n'
270 cmd
= 'NOT %s\r\n' % str(len(m
))
272 # Whee, lots of fun to test that lineReceived & dataReceived work well with input coming
273 # in in (fairly) arbitrary chunks.
274 map(self
.client
.dataReceived
, [x
+'\r\n' for x
in m
.split('\r\n')[:-1]])
275 self
.failUnless((self
.client
.state
== 'NOTIFICATION'), 'Failed to detect MSN Alert message')
277 def testListSync(self
):
278 self
.client
.makeConnection(StringIOWithoutClosing())
279 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
281 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 3" % self
.client
.currentID
,
284 "LSG Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
285 "LSG Other%20Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz",
286 "LSG More%20Other%20Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyya",
287 "LST N=userHandle@email.com F=Some%20Name C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 13 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy,yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz",
289 map(self
.client
.lineReceived
, lines
)
290 contacts
= self
.client
.factory
.contacts
291 contact
= contacts
.getContact('userHandle@email.com')
292 #self.failUnless(contacts.version == 100, "Invalid contact list version")
293 self
.failUnless(contact
.screenName
== 'Some Name', "Invalid screen-name for user")
294 self
.failUnless(contacts
.groups
== {'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy': 'Friends', \
295 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz': 'Other Friends', \
296 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyya': 'More Other Friends'} \
297 , "Did not get proper group list")
298 self
.failUnless(contact
.groups
== ['yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy', \
299 'yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz'] and \
300 contact
.lists
== 13, "Invalid contact list/group info")
301 self
.failUnless(self
.client
.state
== 'GOTLIST', "Failed to call list sync handler")
304 def testStatus(self
):
305 # Set up the contact list
306 self
.client
.makeConnection(StringIOWithoutClosing())
307 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
309 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self
.client
.currentID
,
312 "LST N=foo@bar.com F=Some%20Name C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 13 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy,yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyz",
314 map(self
.client
.lineReceived
, lines
)
316 msnobj
= urllib
.quote('<msnobj Creator="buddy1@hotmail.com" Size="24539" Type="3" Location="TFR2C.tmp" Friendly="AAA=" SHA1D="trC8SlFx2sWQxZMIBAWSEnXc8oQ=" SHA1C="U32o6bosZzluJq82eAtMpx5dIEI="/>')
317 t
= [('ILN 1 AWY foo@bar.com Test%20Screen%20Name 268435456 ' + msnobj
, 'INITSTATUS', 'Failed to detect initial status report'),
318 ('NLN LUN foo@bar.com Test%20Name 0', 'NEWSTATUS', 'Failed to detect contact status change'),
319 ('NLN AWY foo@bar.com Test%20Name 0 ' + msnobj
, 'NEWAVATAR', 'Failed to detect contact avatar change'),
320 ('NLN AWY foo@bar.com Test%20Name 0', 'AVATARGONE', 'Failed to detect contact avatar disappearing'),
321 ('FLN foo@bar.com', 'OFFLINE', 'Failed to detect contact signing off'),
322 ('CHG 1 HDN 0 ' + msnobj
, 'MYSTATUS', 'Failed to detect my status changing')]
324 self
.client
.lineReceived(i
[0])
325 self
.failUnless((self
.client
.state
== i
[1]), i
[2])
328 self
.client
.dataReceived('UBX foo@bar.com 72\r\n<Data><PSM>My Personal Message</PSM><CurrentMedia></CurrentMedia></Data>')
329 self
.failUnless((self
.client
.state
== 'GOTPERSONAL'), 'Failed to detect new personal message')
330 self
.client
.dataReceived('UBX foo@bar.com 0\r\n')
331 self
.failUnless((self
.client
.state
== 'PERSONALGONE'), 'Failed to detect personal message disappearing')
334 def testAsyncPhoneChange(self
):
335 c
= msn
.MSNContact(userHandle
='userHandle@email.com')
336 self
.client
.factory
.contacts
= msn
.MSNContactList()
337 self
.client
.factory
.contacts
.addContact(c
)
338 self
.client
.makeConnection(StringIOWithoutClosing())
339 self
.client
.lineReceived("BPR 101 userHandle@email.com PHH 123%20456")
340 c
= self
.client
.factory
.contacts
.getContact('userHandle@email.com')
341 self
.failUnless(self
.client
.state
== 'GOTPHONE', "Did not fire phone change callback")
342 self
.failUnless(c
.homePhone
== '123 456', "Did not update the contact's phone number")
343 self
.failUnless(self
.client
.factory
.contacts
.version
== 101, "Did not update list version")
345 def testLateBPR(self
):
347 This test makes sure that if a BPR response that was meant
348 to be part of a SYN response (but came after the last LST)
349 is received, the correct contact is updated and all is well
351 self
.client
.makeConnection(StringIOWithoutClosing())
352 msn
.NotificationClient
.loggedIn(self
.client
, 'foo@foo.com', 1)
354 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self
.client
.currentID
,
357 "LSG Friends yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
358 "LST N=userHandle@email.com F=Some%20Name C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 13 yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy",
361 map(self
.client
.lineReceived
, lines
)
362 contact
= self
.client
.factory
.contacts
.getContact('userHandle@email.com')
363 self
.failUnless(contact
.homePhone
== '123 456', "Did not update contact's phone number")
366 def testUserRemovedMe(self
):
367 self
.client
.factory
.contacts
= msn
.MSNContactList()
368 contact
= msn
.MSNContact(userHandle
='foo@foo.com')
369 contact
.addToList(msn
.REVERSE_LIST
)
370 self
.client
.factory
.contacts
.addContact(contact
)
371 self
.client
.lineReceived("REM 0 RL foo@foo.com")
372 self
.failUnless(self
.client
.state
== 'USERREMOVEDME', "Failed to remove user from reverse list")
374 def testUserAddedMe(self
):
375 self
.client
.factory
.contacts
= msn
.MSNContactList()
376 self
.client
.lineReceived("ADC 0 RL N=foo@foo.com F=Screen%20Name")
377 self
.failUnless(self
.client
.state
== 'USERADDEDME', "Failed to add user to reverse lise")
379 def testAsyncSwitchboardInvitation(self
):
380 self
.client
.lineReceived("RNG 1234 192.168.1.1:1863 CKI 123.456 foo@foo.com Screen%20Name")
381 self
.failUnless((self
.client
.state
== 'SBINVITED'), 'Failed to detect switchboard invitation')
384 #######################################
385 # Notification with fake server tests #
386 #######################################
388 class FakeNotificationServer(msn
.MSNEventBase
):
389 def handle_CHG(self
, params
):
392 self
.sendLine("CHG %s %s %s %s" % (params
[0], params
[1], params
[2], params
[3]))
394 def handle_BLP(self
, params
):
395 self
.sendLine("BLP %s %s 100" % (params
[0], params
[1]))
397 def handle_ADC(self
, params
):
399 list = msn
.listCodeToID
[params
[1].lower()]
400 if list == msn
.FORWARD_LIST
:
414 if userHandle
and userGuid
:
415 self
.transport
.loseConnection()
419 self
.transport
.loseConnection()
421 self
.sendLine("ADC %s FL N=%s F=%s C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx %s" % (trid
, userHandle
, screenName
, groups
))
424 raise "NotImplementedError"
427 self
.transport
.loseConnection()
428 if not params
[2].startswith("N=") and params
[2].count('@') == 1:
429 self
.transport
.loseConnection()
430 self
.sendLine("ADC %s %s %s" % (params
[0], params
[1], params
[2]))
432 def handle_REM(self
, params
):
434 self
.transport
.loseConnection()
437 trid
= int(params
[0])
438 listType
= msn
.listCodeToID
[params
[1].lower()]
440 self
.transport
.loseConnection()
441 if listType
== msn
.FORWARD_LIST
and params
[2].count('@') > 0:
442 self
.transport
.loseConnection()
443 elif listType
!= msn
.FORWARD_LIST
and params
[2].count('@') != 1:
444 self
.transport
.loseConnection()
446 self
.sendLine("REM %s %s %s" % (params
[0], params
[1], params
[2]))
448 def handle_PRP(self
, params
):
450 self
.transport
.loseConnection()
451 if params
[1] == "MFN":
452 self
.sendLine("PRP %s MFN %s" % (params
[0], params
[2]))
454 # Only friendly names are implemented
455 self
.transport
.loseConnection()
457 def handle_UUX(self
, params
):
459 self
.transport
.loseConnection()
463 self
.currentMessage
= msn
.MSNMessage(length
=l
, userHandle
=params
[0], screenName
="UUX", specialMessage
=True)
466 self
.sendLine("UUX %s 0" % params
[0])
468 def checkMessage(self
, message
):
469 if message
.specialMessage
:
470 if message
.screenName
== "UUX":
471 self
.sendLine("UUX %s 0" % message
.userHandle
)
475 def handle_XFR(self
, params
):
477 self
.transport
.loseConnection()
479 if params
[1] != "SB":
480 self
.transport
.loseConnection()
482 self
.sendLine("XFR %s SB 129.129.129.129:1234 CKI SomeSecret" % params
[0])
486 class FakeNotificationClient(msn
.NotificationClient
):
487 def doStatusChange(self
):
488 def testcb((status
,)):
489 if status
== msn
.STATUS_AWAY
:
491 self
.transport
.loseConnection()
492 d
= self
.changeStatus(msn
.STATUS_AWAY
)
493 d
.addCallback(testcb
)
495 def doPrivacyMode(self
):
497 if priv
.upper() == 'AL':
499 self
.transport
.loseConnection()
500 d
= self
.setPrivacyMode(True)
501 d
.addCallback(testcb
)
503 def doAddContactFL(self
):
504 def testcb((listType
, userGuid
, userHandle
, screenName
)):
505 if listType
& msn
.FORWARD_LIST
and \
506 userGuid
== "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" and \
507 userHandle
== "foo@bar.com" and \
508 screenName
== "foo@bar.com" and \
509 self
.factory
.contacts
.getContact(userHandle
):
511 self
.transport
.loseConnection()
512 d
= self
.addContact(msn
.FORWARD_LIST
, "foo@bar.com")
513 d
.addCallback(testcb
)
515 def doAddContactAL(self
):
516 def testcb((listType
, userGuid
, userHandle
, screenName
)):
517 if listType
& msn
.ALLOW_LIST
and \
518 userHandle
== "foo@bar.com" and \
519 not userGuid
and not screenName
and \
520 self
.factory
.contacts
.getContact(userHandle
):
522 self
.transport
.loseConnection()
523 d
= self
.addContact(msn
.ALLOW_LIST
, "foo@bar.com")
524 d
.addCallback(testcb
)
526 def doRemContactFL(self
):
527 def testcb((listType
, userHandle
, groupID
)):
528 if listType
& msn
.FORWARD_LIST
and \
529 userHandle
== "foo@bar.com":
531 self
.transport
.loseConnection()
532 d
= self
.remContact(msn
.FORWARD_LIST
, "foo@bar.com")
533 d
.addCallback(testcb
)
535 def doRemContactAL(self
):
536 def testcb((listType
, userHandle
, groupID
)):
537 if listType
& msn
.ALLOW_LIST
and \
538 userHandle
== "foo@bar.com":
540 self
.transport
.loseConnection()
541 d
= self
.remContact(msn
.ALLOW_LIST
, "foo@bar.com")
542 d
.addCallback(testcb
)
544 def doScreenNameChange(self
):
545 def testcb((screenName
,)):
546 if screenName
== "Some new name":
548 self
.transport
.loseConnection()
549 d
= self
.changeScreenName("Some new name")
550 d
.addCallback(testcb
)
552 def doPersonalChange(self
, personal
):
553 def testcb((checkPersonal
,)):
554 if checkPersonal
== personal
:
556 self
.transport
.loseConnection()
557 d
= self
.changePersonalMessage(personal
)
558 d
.addCallback(testcb
)
560 def doAvatarChange(self
, data
):
563 self
.transport
.loseConnection()
564 d
= self
.changeAvatar(data
, True)
565 d
.addCallback(testcb
)
567 def doRequestSwitchboard(self
):
568 def testcb((host
, port
, key
)):
569 if host
== "129.129.129.129" and port
== 1234 and key
== "SomeSecret":
571 self
.transport
.loseConnection()
572 d
= self
.requestSwitchboardServer()
573 d
.addCallback(testcb
)
575 class FakeServerNotificationTests(unittest
.TestCase
):
576 """ tests the NotificationClient against a fake server. """
579 self
.client
= FakeNotificationClient()
580 self
.client
.factory
= msn
.NotificationFactory()
581 self
.client
.test
= 'FAIL'
582 self
.server
= FakeNotificationServer()
583 self
.loop
= LoopbackCon(self
.client
, self
.server
)
586 self
.loop
.disconnect()
588 def testChangeStatus(self
):
589 self
.client
.doStatusChange()
590 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
591 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change status properly')
593 def testSetPrivacyMode(self
):
594 self
.client
.factory
.contacts
= msn
.MSNContactList()
595 self
.client
.doPrivacyMode()
596 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
597 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change privacy mode')
599 def testSyncList(self
):
600 self
.client
.doSyncList()
601 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
602 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to synchronise list')
603 testSyncList
.skip
= "Will do after list versions."
605 def testAddContactFL(self
):
606 self
.client
.factory
.contacts
= msn
.MSNContactList()
607 self
.client
.doAddContactFL()
608 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
609 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to add contact to forward list')
611 def testAddContactAL(self
):
612 self
.client
.factory
.contacts
= msn
.MSNContactList()
613 self
.client
.doAddContactAL()
614 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
615 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to add contact to allow list')
617 def testRemContactFL(self
):
618 self
.client
.factory
.contacts
= msn
.MSNContactList()
619 self
.client
.factory
.contacts
.addContact(msn
.MSNContact(userGuid
="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", userHandle
="foo@bar.com", screenName
="Some guy", lists
=msn
.FORWARD_LIST
))
620 self
.client
.doRemContactFL()
621 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
622 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to remove contact from forward list')
624 def testRemContactAL(self
):
625 self
.client
.factory
.contacts
= msn
.MSNContactList()
626 self
.client
.factory
.contacts
.addContact(msn
.MSNContact(userGuid
="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", userHandle
="foo@bar.com", screenName
="Some guy", lists
=msn
.ALLOW_LIST
))
627 self
.client
.doRemContactAL()
628 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
629 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to remove contact from allow list')
631 def testChangedScreenName(self
):
632 self
.client
.doScreenNameChange()
633 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
634 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change screen name properly')
636 def testChangePersonal1(self
):
637 self
.client
.doPersonalChange("Some personal message")
638 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
639 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change personal message properly')
641 def testChangePersonal2(self
):
642 self
.client
.doPersonalChange("")
643 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
644 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change personal message properly')
646 def testChangeAvatar(self
):
647 self
.client
.doAvatarChange("DATADATADATADATA")
648 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
649 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change avatar properly')
651 def testRequestSwitchboard(self
):
652 self
.client
.doRequestSwitchboard()
653 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
654 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to request switchboard')
657 #################################
658 # Notification challenges tests #
659 #################################
661 class DummyChallengeNotificationServer(msn
.MSNEventBase
):
662 def doChallenge(self
, challenge
, response
):
664 self
.response
= response
665 self
.sendLine("CHL 0 " + challenge
)
667 def checkMessage(self
, message
):
668 if message
.message
== self
.response
:
670 self
.transport
.loseConnection()
673 def handle_QRY(self
, params
):
675 if len(params
) == 3 and params
[1] == "PROD0090YUAUV{2B" and params
[2] == "32":
677 self
.currentMessage
= msn
.MSNMessage(length
=32, userHandle
="QRY", screenName
="QRY", specialMessage
=True)
680 self
.transport
.loseConnection()
682 class DummyChallengeNotificationClient(msn
.NotificationClient
):
683 def connectionMade(self
):
684 msn
.MSNEventBase
.connectionMade(self
)
686 def handle_CHL(self
, params
):
687 msn
.NotificationClient
.handle_CHL(self
, params
)
688 self
.transport
.loseConnection()
691 class NotificationChallengeTests(unittest
.TestCase
):
692 """ tests the responses to the CHLs the server sends """
695 self
.client
= DummyChallengeNotificationClient()
696 self
.server
= DummyChallengeNotificationServer()
697 self
.loop
= LoopbackCon(self
.client
, self
.server
)
700 self
.loop
.disconnect()
702 def testChallenges(self
):
703 challenges
= [('13038318816579321232', 'b01c13020e374d4fa20abfad6981b7a9'),
704 ('23055170411503520698', 'ae906c3f2946d25e7da1b08b0b247659'),
705 ('37819769320541083311', 'db79d37dadd9031bef996893321da480'),
706 ('93662730714769834295', 'd619dfbb1414004d34d0628766636568'),
707 ('31154116582196216093', '95e96c4f8cfdba6f065c8869b5e984e9')]
708 for challenge
, response
in challenges
:
709 self
.server
.doChallenge(challenge
, response
)
710 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
711 self
.failUnless((self
.server
.state
== 'PASS'), 'Incorrect challenge response.')
714 ###########################
715 # Notification ping tests #
716 ###########################
718 class DummyPingNotificationServer(LineReceiver
):
719 def lineReceived(self
, line
):
720 if line
.startswith("PNG") and self
.good
:
721 self
.sendLine("QNG 50")
723 class DummyPingNotificationClient(msn
.NotificationClient
):
724 def connectionMade(self
):
725 self
.pingCheckerStart()
727 def sendLine(self
, line
):
728 msn
.NotificationClient
.sendLine(self
, line
)
731 self
.transport
.loseConnection() # But not for real, just to end the test
733 def connectionLost(self
, reason
):
735 self
.state
= 'DISCONNECTED'
737 class NotificationPingTests(unittest
.TestCase
):
738 """ tests pinging in the NotificationClient class """
742 self
.client
= DummyPingNotificationClient()
743 self
.server
= DummyPingNotificationServer()
744 self
.client
.state
= 'CONNECTED'
745 self
.client
.count
= 0
746 self
.loop
= LoopbackCon(self
.client
, self
.server
)
751 self
.loop
.disconnect()
753 def testPingGood(self
):
754 self
.server
.good
= True
755 self
.loop
.doSteps(100)
756 self
.failUnless((self
.client
.state
== 'CONNECTED'), 'Should be connected.')
758 def testPingBad(self
):
759 self
.server
.good
= False
760 self
.loop
.doSteps(100)
761 self
.failUnless((self
.client
.state
== 'DISCONNECTED'), 'Should be disconnected.')
766 ###########################
767 # Switchboard basic tests #
768 ###########################
770 class DummySwitchboardServer(msn
.MSNEventBase
):
771 def handle_USR(self
, params
):
773 self
.transport
.loseConnection()
774 if params
[1] == 'foo@bar.com' and params
[2] == 'somekey':
775 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
777 def handle_ANS(self
, params
):
779 self
.transport
.loseConnection()
780 if params
[1] == 'foo@bar.com' and params
[2] == 'somekey' and params
[3] == 'someSID':
781 self
.sendLine("ANS %s OK" % params
[0])
783 def handle_CAL(self
, params
):
785 self
.transport
.loseConnection()
786 if params
[1] == 'friend@hotmail.com':
787 self
.sendLine("CAL %s RINGING 1111122" % params
[0])
789 self
.transport
.loseConnection()
791 def checkMessage(self
, message
):
792 if message
.message
== 'Hi how are you today?':
793 self
.sendLine("ACK " + message
.userHandle
) # Relies on TRID getting stored in userHandle trick
795 self
.transport
.loseConnection()
798 class DummySwitchboardClient(msn
.SwitchboardClient
):
800 self
.state
= 'LOGGEDIN'
801 self
.transport
.loseConnection()
803 def gotChattingUsers(self
, users
):
804 if users
== {'fred@hotmail.com': 'fred', 'jack@email.com': 'jack has a nickname!'}:
805 self
.state
= 'GOTCHATTINGUSERS'
807 def userJoined(self
, userHandle
, screenName
):
808 if userHandle
== "friend@hotmail.com" and screenName
== "friend nickname":
809 self
.state
= 'USERJOINED'
811 def userLeft(self
, userHandle
):
812 if userHandle
== "friend@hotmail.com":
813 self
.state
= 'USERLEFT'
815 def gotContactTyping(self
, message
):
816 if message
.userHandle
== 'foo@bar.com':
817 self
.state
= 'USERTYPING'
819 def gotMessage(self
, message
):
820 if message
.userHandle
== 'friend@hotmail.com' and \
821 message
.screenName
== 'Friend Nickname' and \
822 message
.message
== 'Hello.':
823 self
.state
= 'GOTMESSAGE'
825 def doSendInvite(self
):
828 self
.state
= 'INVITESUCCESS'
829 self
.transport
.loseConnection()
830 d
= self
.inviteUser('friend@hotmail.com')
831 d
.addCallback(testcb
)
833 def doSendMessage(self
):
835 self
.state
= 'MESSAGESUCCESS'
836 self
.transport
.loseConnection()
838 m
.setHeader("Content-Type", "text/plain; charset=UTF-8")
839 m
.message
= 'Hi how are you today?'
840 m
.ack
= msn
.MSNMessage
.MESSAGE_ACK
841 d
= self
.sendMessage(m
)
842 d
.addCallback(testcb
)
845 class SwitchboardBasicTests(unittest
.TestCase
):
846 """ Tests basic functionality of switchboard sessions """
848 self
.client
= DummySwitchboardClient()
849 self
.client
.state
= 'START'
850 self
.client
.userHandle
= 'foo@bar.com'
851 self
.client
.key
= 'somekey'
852 self
.client
.sessionID
= 'someSID'
853 self
.server
= DummySwitchboardServer()
854 self
.loop
= LoopbackCon(self
.client
, self
.server
)
857 self
.loop
.disconnect()
859 def _testSB(self
, reply
):
860 self
.client
.reply
= reply
861 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
862 self
.failUnless((self
.client
.state
== 'LOGGEDIN'), 'Failed to login with reply='+str(reply
))
870 def testChattingUsers(self
):
871 lines
= ["IRO 1 1 2 fred@hotmail.com fred",
872 "IRO 1 2 2 jack@email.com jack%20has%20a%20nickname%21"]
874 self
.client
.lineReceived(line
)
875 self
.failUnless((self
.client
.state
== 'GOTCHATTINGUSERS'), 'Failed to get chatting users')
877 def testUserJoined(self
):
878 self
.client
.lineReceived("JOI friend@hotmail.com friend%20nickname")
879 self
.failUnless((self
.client
.state
== 'USERJOINED'), 'Failed to notice user joining')
881 def testUserLeft(self
):
882 self
.client
.lineReceived("BYE friend@hotmail.com")
883 self
.failUnless((self
.client
.state
== 'USERLEFT'), 'Failed to notice user leaving')
885 def testTypingCheck(self
):
886 m
= 'MSG foo@bar.com Foo 80\r\n'
887 m
+= 'MIME-Version: 1.0\r\n'
888 m
+= 'Content-Type: text/x-msmsgscontrol\r\n'
889 m
+= 'TypingUser: foo@bar\r\n'
891 self
.client
.dataReceived(m
)
892 self
.failUnless((self
.client
.state
== 'USERTYPING'), 'Failed to detect typing notification')
894 def testGotMessage(self
):
895 m
= 'MSG friend@hotmail.com Friend%20Nickname 68\r\n'
896 m
+= 'MIME-Version: 1.0\r\n'
897 m
+= 'Content-Type: text/plain; charset=UTF-8\r\n'
899 self
.client
.dataReceived(m
)
900 self
.failUnless((self
.client
.state
== 'GOTMESSAGE'), 'Failed to detect message')
902 def testInviteUser(self
):
903 self
.client
.connectionMade
= lambda: None
904 self
.client
.doSendInvite()
905 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
906 self
.failUnless((self
.client
.state
== 'INVITESUCCESS'), 'Failed to invite user')
908 def testSendMessage(self
):
909 self
.client
.connectionMade
= lambda: None
910 self
.client
.doSendMessage()
911 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
912 self
.failUnless((self
.client
.state
== 'MESSAGESUCCESS'), 'Failed to send message')
919 class DummySwitchboardP2PServerHelper(msn
.MSNEventBase
):
920 def __init__(self
, server
):
921 msn
.MSNEventBase
.__init
__(self
)
924 def handle_USR(self
, params
):
926 self
.transport
.loseConnection()
927 self
.userHandle
= params
[1]
928 if params
[1] == 'foo1@bar.com' and params
[2] == 'somekey1':
929 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
930 if params
[1] == 'foo2@bar.com' and params
[2] == 'somekey2':
931 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
933 def checkMessage(self
, message
):
936 def gotMessage(self
, message
):
937 message
.userHandle
= self
.userHandle
938 message
.screenName
= self
.userHandle
939 self
.server
.gotMessage(message
, self
)
941 def sendMessage(self
, message
):
942 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
943 self
.sendLine("MSG %s %s %s" % (message
.userHandle
, message
.screenName
, message
.length
))
944 self
.sendLine('MIME-Version: %s' % message
.getHeader('MIME-Version'))
945 self
.sendLine('Content-Type: %s' % message
.getHeader('Content-Type'))
946 for header
in [h
for h
in message
.headers
.items() if h
[0].lower() not in ('mime-version','content-type')]:
947 self
.sendLine("%s: %s" % (header
[0], header
[1]))
948 self
.transport
.write("\r\n")
949 self
.transport
.write(message
.message
)
952 class DummySwitchboardP2PServer
:
957 c
= DummySwitchboardP2PServerHelper(self
)
958 self
.clients
.append(c
)
961 def gotMessage(self
, message
, sender
):
962 for c
in self
.clients
:
964 c
.sendMessage(message
)
966 class DummySwitchboardP2PClient(msn
.SwitchboardClient
):
967 def gotMessage(self
, message
):
968 if message
.message
== "Test Message" and message
.userHandle
== "foo1@bar.com":
969 self
.status
= "GOTMESSAGE"
971 def gotFileReceive(self
, fileReceive
):
972 self
.fileReceive
= fileReceive
974 class SwitchboardP2PTests(unittest
.TestCase
):
976 self
.server
= DummySwitchboardP2PServer()
977 self
.client1
= DummySwitchboardP2PClient()
978 self
.client1
.key
= 'somekey1'
979 self
.client1
.userHandle
= 'foo1@bar.com'
980 self
.client2
= DummySwitchboardP2PClient()
981 self
.client2
.key
= 'somekey2'
982 self
.client2
.userHandle
= 'foo2@bar.com'
983 self
.client2
.status
= "INIT"
984 self
.loop1
= LoopbackCon(self
.client1
, self
.server
.newClient())
985 self
.loop2
= LoopbackCon(self
.client2
, self
.server
.newClient())
988 self
.loop1
.disconnect()
989 self
.loop2
.disconnect()
991 def _loop(self
, steps
=1):
992 for i
in xrange(steps
):
993 self
.loop1
.doSteps(1)
994 self
.loop2
.doSteps(1)
996 def testMessage(self
):
997 self
.client1
.sendMessage(msn
.MSNMessage(message
='Test Message'))
999 self
.failUnless((self
.client2
.status
== "GOTMESSAGE"), "Fake switchboard server not working.")
1001 def _generateData(self
):
1003 for i
in xrange(3000):
1004 data
+= struct
.pack("<L", random
.randint(0, sys
.maxint
))
1007 def testAvatars(self
):
1008 self
.gotAvatar
= False
1010 # Set up the avatar for client1
1011 imageData
= self
._generateData
()
1012 self
.client1
.msnobj
= msn
.MSNObject()
1013 self
.client1
.msnobj
.setData('foo1@bar.com', imageData
)
1014 self
.client1
.msnobj
.makeText()
1016 # Make client2 request the avatar
1017 def avatarCallback((data
,)):
1018 self
.gotAvatar
= (data
== imageData
)
1019 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', msnobj
=self
.client1
.msnobj
)
1020 d
= self
.client2
.sendAvatarRequest(msnContact
)
1021 d
.addCallback(avatarCallback
)
1023 # Let them do their thing
1026 # Check that client2 got the avatar
1027 self
.failUnless((self
.gotAvatar
), "Failed to transfer avatar")
1029 def testFilesHappyPath(self
):
1030 fileData
= self
._generateData
()
1031 self
.gotFile
= False
1033 # Send the file (client2->client1)
1034 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1035 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1036 def accepted((yes
,)):
1038 fileSend
.write(fileData
)
1041 raise "TransferDeclined"
1043 raise "TransferError"
1044 d
.addCallback(accepted
)
1045 d
.addErrback(failed
)
1047 # Let the request get pushed to client1
1052 self
.gotFile
= (data
== fileData
)
1053 fileBuffer
= msn
.StringBuffer(finished
)
1054 fileReceive
= self
.client1
.fileReceive
1055 self
.failUnless((fileReceive
.filename
== "myfile.txt" and fileReceive
.filesize
== len(fileData
)), "Filename or length wrong.")
1056 fileReceive
.accept(fileBuffer
)
1061 self
.failUnless((self
.gotFile
), "Failed to transfer file")
1063 def testFilesDeclinePath(self
):
1064 fileData
= self
._generateData
()
1065 self
.gotDecline
= False
1067 # Send the file (client2->client1)
1068 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1069 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1070 def accepted((yes
,)):
1071 self
.failUnless((not yes
), "Failed to understand a decline.")
1072 self
.gotDecline
= True
1074 raise "TransferError"
1075 d
.addCallback(accepted
)
1076 d
.addErrback(failed
)
1078 # Let the request get pushed to client1
1082 fileReceive
= self
.client1
.fileReceive
1083 fileReceive
.reject()
1085 # Let the decline get pushed to client2
1088 self
.failUnless((self
.gotDecline
), "Failed to understand a decline, ignored.")
1095 class FileTransferTestCase(unittest
.TestCase
):
1096 """ test FileSend against FileReceive """
1099 self
.input = StringIOWithoutClosing()
1100 self
.input.writelines(['a'] * 7000)
1102 self
.output
= StringIOWithoutClosing()
1108 def testFileTransfer(self
):
1110 sender
= msnft
.MSNFTP_FileSend(self
.input)
1112 sender
.fileSize
= 7000
1113 client
= msnft
.MSNFTP_FileReceive(auth
, "foo@bar.com", self
.output
)
1114 client
.fileSize
= 7000
1115 loop
= LoopbackCon(client
, sender
)
1117 self
.failUnless((client
.completed
and sender
.completed
), "send failed to complete")
1118 self
.failUnless((self
.input.getvalue() == self
.output
.getvalue()), "saved file does not match original")