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
):
547 self
.transport
.loseConnection()
548 d
= self
.changeScreenName("Some new name")
549 d
.addCallback(testcb
)
551 def doPersonalChange(self
, personal
):
552 def testcb((checkPersonal
,)):
553 if checkPersonal
== personal
:
555 self
.transport
.loseConnection()
556 d
= self
.changePersonalMessage(personal
)
557 d
.addCallback(testcb
)
559 def doAvatarChange(self
, data
):
562 self
.transport
.loseConnection()
563 d
= self
.changeAvatar(data
, True)
564 d
.addCallback(testcb
)
566 def doRequestSwitchboard(self
):
567 def testcb((host
, port
, key
)):
568 if host
== "129.129.129.129" and port
== 1234 and key
== "SomeSecret":
570 self
.transport
.loseConnection()
571 d
= self
.requestSwitchboardServer()
572 d
.addCallback(testcb
)
574 class FakeServerNotificationTests(unittest
.TestCase
):
575 """ tests the NotificationClient against a fake server. """
578 self
.client
= FakeNotificationClient()
579 self
.client
.factory
= msn
.NotificationFactory()
580 self
.client
.test
= 'FAIL'
581 self
.server
= FakeNotificationServer()
582 self
.loop
= LoopbackCon(self
.client
, self
.server
)
585 self
.loop
.disconnect()
587 def testChangeStatus(self
):
588 self
.client
.doStatusChange()
589 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
590 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change status properly')
592 def testSetPrivacyMode(self
):
593 self
.client
.factory
.contacts
= msn
.MSNContactList()
594 self
.client
.doPrivacyMode()
595 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
596 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change privacy mode')
598 def testSyncList(self
):
599 self
.client
.doSyncList()
600 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
601 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to synchronise list')
602 testSyncList
.skip
= "Will do after list versions."
604 def testAddContactFL(self
):
605 self
.client
.factory
.contacts
= msn
.MSNContactList()
606 self
.client
.doAddContactFL()
607 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
608 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to add contact to forward list')
610 def testAddContactAL(self
):
611 self
.client
.factory
.contacts
= msn
.MSNContactList()
612 self
.client
.doAddContactAL()
613 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
614 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to add contact to allow list')
616 def testRemContactFL(self
):
617 self
.client
.factory
.contacts
= msn
.MSNContactList()
618 self
.client
.factory
.contacts
.addContact(msn
.MSNContact(userGuid
="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", userHandle
="foo@bar.com", screenName
="Some guy", lists
=msn
.FORWARD_LIST
))
619 self
.client
.doRemContactFL()
620 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
621 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to remove contact from forward list')
623 def testRemContactAL(self
):
624 self
.client
.factory
.contacts
= msn
.MSNContactList()
625 self
.client
.factory
.contacts
.addContact(msn
.MSNContact(userGuid
="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", userHandle
="foo@bar.com", screenName
="Some guy", lists
=msn
.ALLOW_LIST
))
626 self
.client
.doRemContactAL()
627 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
628 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to remove contact from allow list')
630 def testChangedScreenName(self
):
631 self
.client
.doScreenNameChange()
632 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
633 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change screen name properly')
635 def testChangePersonal1(self
):
636 self
.client
.doPersonalChange("Some personal message")
637 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
638 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change personal message properly')
640 def testChangePersonal2(self
):
641 self
.client
.doPersonalChange("")
642 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
643 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change personal message properly')
645 def testChangeAvatar(self
):
646 self
.client
.doAvatarChange("DATADATADATADATA")
647 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
648 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to change avatar properly')
650 def testRequestSwitchboard(self
):
651 self
.client
.doRequestSwitchboard()
652 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
653 self
.failUnless((self
.client
.test
== 'PASS'), 'Failed to request switchboard')
656 #################################
657 # Notification challenges tests #
658 #################################
660 class DummyChallengeNotificationServer(msn
.MSNEventBase
):
661 def doChallenge(self
, challenge
, response
):
663 self
.response
= response
664 self
.sendLine("CHL 0 " + challenge
)
666 def checkMessage(self
, message
):
667 if message
.message
== self
.response
:
669 self
.transport
.loseConnection()
672 def handle_QRY(self
, params
):
674 if len(params
) == 3 and params
[1] == "PROD0090YUAUV{2B" and params
[2] == "32":
676 self
.currentMessage
= msn
.MSNMessage(length
=32, userHandle
="QRY", screenName
="QRY", specialMessage
=True)
679 self
.transport
.loseConnection()
681 class DummyChallengeNotificationClient(msn
.NotificationClient
):
682 def connectionMade(self
):
683 msn
.MSNEventBase
.connectionMade(self
)
685 def handle_CHL(self
, params
):
686 msn
.NotificationClient
.handle_CHL(self
, params
)
687 self
.transport
.loseConnection()
690 class NotificationChallengeTests(unittest
.TestCase
):
691 """ tests the responses to the CHLs the server sends """
694 self
.client
= DummyChallengeNotificationClient()
695 self
.server
= DummyChallengeNotificationServer()
696 self
.loop
= LoopbackCon(self
.client
, self
.server
)
699 self
.loop
.disconnect()
701 def testChallenges(self
):
702 challenges
= [('13038318816579321232', 'b01c13020e374d4fa20abfad6981b7a9'),
703 ('23055170411503520698', 'ae906c3f2946d25e7da1b08b0b247659'),
704 ('37819769320541083311', 'db79d37dadd9031bef996893321da480'),
705 ('93662730714769834295', 'd619dfbb1414004d34d0628766636568'),
706 ('31154116582196216093', '95e96c4f8cfdba6f065c8869b5e984e9')]
707 for challenge
, response
in challenges
:
708 self
.server
.doChallenge(challenge
, response
)
709 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
710 self
.failUnless((self
.server
.state
== 'PASS'), 'Incorrect challenge response.')
713 ###########################
714 # Notification ping tests #
715 ###########################
717 class DummyPingNotificationServer(LineReceiver
):
718 def lineReceived(self
, line
):
719 if line
.startswith("PNG") and self
.good
:
720 self
.sendLine("QNG 50")
722 class DummyPingNotificationClient(msn
.NotificationClient
):
723 def connectionMade(self
):
724 self
.pingCheckerStart()
726 def sendLine(self
, line
):
727 msn
.NotificationClient
.sendLine(self
, line
)
730 self
.transport
.loseConnection() # But not for real, just to end the test
732 def connectionLost(self
, reason
):
734 self
.state
= 'DISCONNECTED'
736 class NotificationPingTests(unittest
.TestCase
):
737 """ tests pinging in the NotificationClient class """
741 self
.client
= DummyPingNotificationClient()
742 self
.server
= DummyPingNotificationServer()
743 self
.client
.state
= 'CONNECTED'
744 self
.client
.count
= 0
745 self
.loop
= LoopbackCon(self
.client
, self
.server
)
750 self
.loop
.disconnect()
752 def testPingGood(self
):
753 self
.server
.good
= True
754 self
.loop
.doSteps(100)
755 self
.failUnless((self
.client
.state
== 'CONNECTED'), 'Should be connected.')
757 def testPingBad(self
):
758 self
.server
.good
= False
759 self
.loop
.doSteps(100)
760 self
.failUnless((self
.client
.state
== 'DISCONNECTED'), 'Should be disconnected.')
765 ###########################
766 # Switchboard basic tests #
767 ###########################
769 class DummySwitchboardServer(msn
.MSNEventBase
):
770 def handle_USR(self
, params
):
772 self
.transport
.loseConnection()
773 if params
[1] == 'foo@bar.com' and params
[2] == 'somekey':
774 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
776 def handle_ANS(self
, params
):
778 self
.transport
.loseConnection()
779 if params
[1] == 'foo@bar.com' and params
[2] == 'somekey' and params
[3] == 'someSID':
780 self
.sendLine("ANS %s OK" % params
[0])
782 def handle_CAL(self
, params
):
784 self
.transport
.loseConnection()
785 if params
[1] == 'friend@hotmail.com':
786 self
.sendLine("CAL %s RINGING 1111122" % params
[0])
788 self
.transport
.loseConnection()
790 def checkMessage(self
, message
):
791 if message
.message
== 'Hi how are you today?':
792 self
.sendLine("ACK " + message
.userHandle
) # Relies on TRID getting stored in userHandle trick
794 self
.transport
.loseConnection()
797 class DummySwitchboardClient(msn
.SwitchboardClient
):
799 self
.state
= 'LOGGEDIN'
800 self
.transport
.loseConnection()
802 def gotChattingUsers(self
, users
):
803 if users
== {'fred@hotmail.com': 'fred', 'jack@email.com': 'jack has a nickname!'}:
804 self
.state
= 'GOTCHATTINGUSERS'
806 def userJoined(self
, userHandle
, screenName
):
807 if userHandle
== "friend@hotmail.com" and screenName
== "friend nickname":
808 self
.state
= 'USERJOINED'
810 def userLeft(self
, userHandle
):
811 if userHandle
== "friend@hotmail.com":
812 self
.state
= 'USERLEFT'
814 def gotContactTyping(self
, message
):
815 if message
.userHandle
== 'foo@bar.com':
816 self
.state
= 'USERTYPING'
818 def gotMessage(self
, message
):
819 if message
.userHandle
== 'friend@hotmail.com' and \
820 message
.screenName
== 'Friend Nickname' and \
821 message
.message
== 'Hello.':
822 self
.state
= 'GOTMESSAGE'
824 def doSendInvite(self
):
827 self
.state
= 'INVITESUCCESS'
828 self
.transport
.loseConnection()
829 d
= self
.inviteUser('friend@hotmail.com')
830 d
.addCallback(testcb
)
832 def doSendMessage(self
):
834 self
.state
= 'MESSAGESUCCESS'
835 self
.transport
.loseConnection()
837 m
.setHeader("Content-Type", "text/plain; charset=UTF-8")
838 m
.message
= 'Hi how are you today?'
839 m
.ack
= msn
.MSNMessage
.MESSAGE_ACK
840 d
= self
.sendMessage(m
)
841 d
.addCallback(testcb
)
844 class SwitchboardBasicTests(unittest
.TestCase
):
845 """ Tests basic functionality of switchboard sessions """
847 self
.client
= DummySwitchboardClient()
848 self
.client
.state
= 'START'
849 self
.client
.userHandle
= 'foo@bar.com'
850 self
.client
.key
= 'somekey'
851 self
.client
.sessionID
= 'someSID'
852 self
.server
= DummySwitchboardServer()
853 self
.loop
= LoopbackCon(self
.client
, self
.server
)
856 self
.loop
.disconnect()
858 def _testSB(self
, reply
):
859 self
.client
.reply
= reply
860 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
861 self
.failUnless((self
.client
.state
== 'LOGGEDIN'), 'Failed to login with reply='+str(reply
))
869 def testChattingUsers(self
):
870 lines
= ["IRO 1 1 2 fred@hotmail.com fred",
871 "IRO 1 2 2 jack@email.com jack%20has%20a%20nickname%21"]
873 self
.client
.lineReceived(line
)
874 self
.failUnless((self
.client
.state
== 'GOTCHATTINGUSERS'), 'Failed to get chatting users')
876 def testUserJoined(self
):
877 self
.client
.lineReceived("JOI friend@hotmail.com friend%20nickname")
878 self
.failUnless((self
.client
.state
== 'USERJOINED'), 'Failed to notice user joining')
880 def testUserLeft(self
):
881 self
.client
.lineReceived("BYE friend@hotmail.com")
882 self
.failUnless((self
.client
.state
== 'USERLEFT'), 'Failed to notice user leaving')
884 def testTypingCheck(self
):
885 m
= 'MSG foo@bar.com Foo 80\r\n'
886 m
+= 'MIME-Version: 1.0\r\n'
887 m
+= 'Content-Type: text/x-msmsgscontrol\r\n'
888 m
+= 'TypingUser: foo@bar\r\n'
890 self
.client
.dataReceived(m
)
891 self
.failUnless((self
.client
.state
== 'USERTYPING'), 'Failed to detect typing notification')
893 def testGotMessage(self
):
894 m
= 'MSG friend@hotmail.com Friend%20Nickname 68\r\n'
895 m
+= 'MIME-Version: 1.0\r\n'
896 m
+= 'Content-Type: text/plain; charset=UTF-8\r\n'
898 self
.client
.dataReceived(m
)
899 self
.failUnless((self
.client
.state
== 'GOTMESSAGE'), 'Failed to detect message')
901 def testInviteUser(self
):
902 self
.client
.connectionMade
= lambda: None
903 self
.client
.doSendInvite()
904 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
905 self
.failUnless((self
.client
.state
== 'INVITESUCCESS'), 'Failed to invite user')
907 def testSendMessage(self
):
908 self
.client
.connectionMade
= lambda: None
909 self
.client
.doSendMessage()
910 self
.failUnless(self
.loop
.doSteps(10), 'Failed to disconnect')
911 self
.failUnless((self
.client
.state
== 'MESSAGESUCCESS'), 'Failed to send message')
918 class DummySwitchboardP2PServerHelper(msn
.MSNEventBase
):
919 def __init__(self
, server
):
920 msn
.MSNEventBase
.__init
__(self
)
923 def handle_USR(self
, params
):
925 self
.transport
.loseConnection()
926 self
.userHandle
= params
[1]
927 if params
[1] == 'foo1@bar.com' and params
[2] == 'somekey1':
928 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
929 if params
[1] == 'foo2@bar.com' and params
[2] == 'somekey2':
930 self
.sendLine("USR %s OK %s %s" % (params
[0], params
[1], params
[1]))
932 def checkMessage(self
, message
):
935 def gotMessage(self
, message
):
936 message
.userHandle
= self
.userHandle
937 message
.screenName
= self
.userHandle
938 self
.server
.gotMessage(message
, self
)
940 def sendMessage(self
, message
):
941 if message
.length
== 0: message
.length
= message
._calcMessageLen
()
942 self
.sendLine("MSG %s %s %s" % (message
.userHandle
, message
.screenName
, message
.length
))
943 self
.sendLine('MIME-Version: %s' % message
.getHeader('MIME-Version'))
944 self
.sendLine('Content-Type: %s' % message
.getHeader('Content-Type'))
945 for header
in [h
for h
in message
.headers
.items() if h
[0].lower() not in ('mime-version','content-type')]:
946 self
.sendLine("%s: %s" % (header
[0], header
[1]))
947 self
.transport
.write("\r\n")
948 self
.transport
.write(message
.message
)
951 class DummySwitchboardP2PServer
:
956 c
= DummySwitchboardP2PServerHelper(self
)
957 self
.clients
.append(c
)
960 def gotMessage(self
, message
, sender
):
961 for c
in self
.clients
:
963 c
.sendMessage(message
)
965 class DummySwitchboardP2PClient(msn
.SwitchboardClient
):
966 def gotMessage(self
, message
):
967 if message
.message
== "Test Message" and message
.userHandle
== "foo1@bar.com":
968 self
.status
= "GOTMESSAGE"
970 def gotFileReceive(self
, fileReceive
):
971 self
.fileReceive
= fileReceive
973 class SwitchboardP2PTests(unittest
.TestCase
):
975 self
.server
= DummySwitchboardP2PServer()
976 self
.client1
= DummySwitchboardP2PClient()
977 self
.client1
.key
= 'somekey1'
978 self
.client1
.userHandle
= 'foo1@bar.com'
979 self
.client2
= DummySwitchboardP2PClient()
980 self
.client2
.key
= 'somekey2'
981 self
.client2
.userHandle
= 'foo2@bar.com'
982 self
.client2
.status
= "INIT"
983 self
.loop1
= LoopbackCon(self
.client1
, self
.server
.newClient())
984 self
.loop2
= LoopbackCon(self
.client2
, self
.server
.newClient())
987 self
.loop1
.disconnect()
988 self
.loop2
.disconnect()
990 def _loop(self
, steps
=1):
991 for i
in xrange(steps
):
992 self
.loop1
.doSteps(1)
993 self
.loop2
.doSteps(1)
995 def testMessage(self
):
996 self
.client1
.sendMessage(msn
.MSNMessage(message
='Test Message'))
998 self
.failUnless((self
.client2
.status
== "GOTMESSAGE"), "Fake switchboard server not working.")
1000 def _generateData(self
):
1002 for i
in xrange(3000):
1003 data
+= struct
.pack("<L", random
.randint(0, sys
.maxint
))
1006 def testAvatars(self
):
1007 self
.gotAvatar
= False
1009 # Set up the avatar for client1
1010 imageData
= self
._generateData
()
1011 self
.client1
.msnobj
= msn
.MSNObject()
1012 self
.client1
.msnobj
.setData('foo1@bar.com', imageData
)
1013 self
.client1
.msnobj
.makeText()
1015 # Make client2 request the avatar
1016 def avatarCallback((data
,)):
1017 self
.gotAvatar
= (data
== imageData
)
1018 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', msnobj
=self
.client1
.msnobj
)
1019 d
= self
.client2
.sendAvatarRequest(msnContact
)
1020 d
.addCallback(avatarCallback
)
1022 # Let them do their thing
1025 # Check that client2 got the avatar
1026 self
.failUnless((self
.gotAvatar
), "Failed to transfer avatar")
1028 def testFilesHappyPath(self
):
1029 fileData
= self
._generateData
()
1030 self
.gotFile
= False
1032 # Send the file (client2->client1)
1033 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1034 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1035 def accepted((yes
,)):
1037 fileSend
.write(fileData
)
1040 raise "TransferDeclined"
1042 raise "TransferError"
1043 d
.addCallback(accepted
)
1044 d
.addErrback(failed
)
1046 # Let the request get pushed to client1
1051 self
.gotFile
= (data
== fileData
)
1052 fileBuffer
= msn
.StringBuffer(finished
)
1053 fileReceive
= self
.client1
.fileReceive
1054 self
.failUnless((fileReceive
.filename
== "myfile.txt" and fileReceive
.filesize
== len(fileData
)), "Filename or length wrong.")
1055 fileReceive
.accept(fileBuffer
)
1060 self
.failUnless((self
.gotFile
), "Failed to transfer file")
1062 def testFilesDeclinePath(self
):
1063 fileData
= self
._generateData
()
1064 self
.gotDecline
= False
1066 # Send the file (client2->client1)
1067 msnContact
= msn
.MSNContact(userHandle
='foo1@bar.com', caps
=msn
.MSNContact
.MSNC1
)
1068 fileSend
, d
= self
.client2
.sendFile(msnContact
, "myfile.txt", len(fileData
))
1069 def accepted((yes
,)):
1070 self
.failUnless((not yes
), "Failed to understand a decline.")
1071 self
.gotDecline
= True
1073 raise "TransferError"
1074 d
.addCallback(accepted
)
1075 d
.addErrback(failed
)
1077 # Let the request get pushed to client1
1081 fileReceive
= self
.client1
.fileReceive
1082 fileReceive
.reject()
1084 # Let the decline get pushed to client2
1087 self
.failUnless((self
.gotDecline
), "Failed to understand a decline, ignored.")
1094 class FileTransferTestCase(unittest
.TestCase
):
1095 """ test FileSend against FileReceive """
1098 self
.input = StringIOWithoutClosing()
1099 self
.input.writelines(['a'] * 7000)
1101 self
.output
= StringIOWithoutClosing()
1107 def testFileTransfer(self
):
1109 sender
= msnft
.MSNFTP_FileSend(self
.input)
1111 sender
.fileSize
= 7000
1112 client
= msnft
.MSNFTP_FileReceive(auth
, "foo@bar.com", self
.output
)
1113 client
.fileSize
= 7000
1114 loop
= LoopbackCon(client
, sender
)
1116 self
.failUnless((client
.completed
and sender
.completed
), "send failed to complete")
1117 self
.failUnless((self
.input.getvalue() == self
.output
.getvalue()), "saved file does not match original")