]> code.delx.au - pymsnt/blob - src/tlib/msn/test_msn.py
More generic P2P. Avatars almost working in testing suite.
[pymsnt] / src / tlib / msn / test_msn.py
1 # Copyright (c) 2001-2005 Twisted Matrix Laboratories.
2 # Copyright (c) 2005 James Bunton
3 # See LICENSE for details.
4
5 """
6 Test cases for msn.
7 """
8
9 # Twisted imports
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
16
17 # System imports
18 import StringIO, sys, urllib
19
20 import msn
21 import msnft
22
23
24 def printError(f):
25 print f
26
27 class StringIOWithoutClosing(StringIO.StringIO):
28 disconnecting = 0
29 def close(self): pass
30 def loseConnection(self): pass
31
32 class LoopbackCon:
33 def __init__(self, con1, con2):
34 self.con1 = con1
35 self.con2 = con2
36 self.con1ToCon2 = loopback.LoopbackRelay(con1)
37 self.con2ToCon1 = loopback.LoopbackRelay(con2)
38 con2.makeConnection(self.con1ToCon2)
39 con1.makeConnection(self.con2ToCon1)
40 self.connected = True
41
42 def doSteps(self, steps=1):
43 """ Returns true if the connection finished """
44 count = 0
45 while count < steps:
46 reactor.iterate(0.01)
47 self.con1ToCon2.clearBuffer()
48 self.con2ToCon1.clearBuffer()
49 if self.con1ToCon2.shouldLose:
50 self.con1ToCon2.clearBuffer()
51 count = -1
52 break
53 elif self.con2ToCon1.shouldLose:
54 count = -1
55 break
56 else:
57 count += 1
58 if count == -1:
59 self.disconnect()
60 return True
61 return False
62
63 def disconnect(self):
64 if self.connected:
65 self.con1.connectionLost(failure.Failure(main.CONNECTION_DONE))
66 self.con2.connectionLost(failure.Failure(main.CONNECTION_DONE))
67 reactor.iterate()
68
69
70
71
72 ##################
73 # Passport tests #
74 ##################
75
76 class PassportTests(unittest.TestCase):
77
78 def setUp(self):
79 self.result = []
80 self.deferred = Deferred()
81 self.deferred.addCallback(lambda r: self.result.append(r))
82 self.deferred.addErrback(printError)
83
84 def testNexus(self):
85 protocol = msn.PassportNexus(self.deferred, 'https://foobar.com/somepage.quux')
86 headers = {
87 'Content-Length' : '0',
88 'Content-Type' : 'text/html',
89 'PassportURLs' : 'DARealm=Passport.Net,DALogin=login.myserver.com/,DAReg=reg.myserver.com'
90 }
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/")
97
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')
104
105 def testPassportLoginSuccess(self):
106 headers = {
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"
112 }
113 self._doLoginTest('HTTP/1.1 200 OK\r\n', headers)
114 self.failUnless(self.result[0] == (msn.LOGIN_SUCCESS, 'somekey'))
115
116 def testPassportLoginFailure(self):
117 headers = {
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'
122 }
123 self._doLoginTest('HTTP/1.1 401 Unauthorized\r\n', headers)
124 self.failUnless(self.result[0] == (msn.LOGIN_FAILURE, 'the error message'))
125
126 def testPassportLoginRedirect(self):
127 headers = {
128 'Content-Type' : 'text/html',
129 'Authentication-Info' : 'Passport1.4 da-status=redir',
130 'Location' : 'https://newlogin.host.com/'
131 }
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'))
134
135
136
137 ######################
138 # Notification tests #
139 ######################
140
141 class DummyNotificationClient(msn.NotificationClient):
142 def loggedIn(self, userHandle, verified):
143 if userHandle == 'foo@bar.com' and verified:
144 self.state = 'LOGIN'
145
146 def gotProfile(self, message):
147 self.state = 'PROFILE'
148
149 def gotContactStatus(self, code, userHandle, 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'
154
155 def contactStatusChanged(self, code, userHandle, screenName):
156 if code == msn.STATUS_LUNCH and userHandle == "foo@bar.com" and screenName == "Test Name":
157 self.state = 'NEWSTATUS'
158
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'
164
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'
170
171 def contactOffline(self, userHandle):
172 if userHandle == "foo@bar.com": self.state = 'OFFLINE'
173
174 def statusChanged(self, code):
175 if code == msn.STATUS_HIDDEN: self.state = 'MYSTATUS'
176
177 def listSynchronized(self, *args):
178 self.state = 'GOTLIST'
179
180 def gotPhoneNumber(self, userHandle, phoneType, number):
181 self.state = 'GOTPHONE'
182
183 def userRemovedMe(self, userHandle):
184 c = self.factory.contacts.getContact(userHandle)
185 if not c: self.state = 'USERREMOVEDME'
186
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'
191
192 def gotSwitchboardInvitation(self, sessionID, host, port, key, userHandle, screenName):
193 if sessionID == 1234 and \
194 host == '192.168.1.1' and \
195 port == 1863 and \
196 key == '123.456' and \
197 userHandle == 'foo@foo.com' and \
198 screenName == 'Screen Name':
199 self.state = 'SBINVITED'
200
201 def gotMSNAlert(self, body, action, subscr):
202 self.state = 'NOTIFICATION'
203
204 def gotInitialEmailNotification(self, inboxunread, foldersunread):
205 if inboxunread == 1 and foldersunread == 0:
206 self.state = 'INITEMAIL1'
207 else:
208 self.state = 'INITEMAIL2'
209
210 def gotRealtimeEmailNotification(self, mailfrom, fromaddr, subject):
211 if mailfrom == 'Some Person' and fromaddr == 'example@passport.com' and subject == 'newsubject':
212 self.state = 'REALTIMEEMAIL'
213
214 class NotificationTests(unittest.TestCase):
215 """ testing the various events in NotificationClient """
216
217 def setUp(self):
218 self.client = DummyNotificationClient()
219 self.client.factory = msn.NotificationFactory()
220 self.client.state = 'START'
221
222 def tearDown(self):
223 self.client = None
224
225 def testLogin(self):
226 self.client.lineReceived('USR 1 OK foo@bar.com 1')
227 self.failUnless((self.client.state == 'LOGIN'), 'Failed to detect successful login')
228
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')
236
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')
244
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')
252
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')
260
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))
271 m = cmd + 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')
276
277 def testListSync(self):
278 self.client.makeConnection(StringIOWithoutClosing())
279 msn.NotificationClient.loggedIn(self.client, 'foo@foo.com', 1)
280 lines = [
281 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 3" % self.client.currentID,
282 "GTC A",
283 "BLP AL",
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",
288 ]
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")
302 self.client.logOut()
303
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)
308 lines = [
309 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self.client.currentID,
310 "GTC A",
311 "BLP AL",
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",
313 ]
314 map(self.client.lineReceived, lines)
315 # Now test!
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')]
323 for i in t:
324 self.client.lineReceived(i[0])
325 self.failUnless((self.client.state == i[1]), i[2])
326
327 # Test UBX
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')
332 self.client.logOut()
333
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")
344
345 def testLateBPR(self):
346 """
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
350 """
351 self.client.makeConnection(StringIOWithoutClosing())
352 msn.NotificationClient.loggedIn(self.client, 'foo@foo.com', 1)
353 lines = [
354 "SYN %s 2005-04-23T18:57:44.8130000-07:00 2005-04-23T18:57:54.2070000-07:00 1 0" % self.client.currentID,
355 "GTC A",
356 "BLP AL",
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",
359 "BPR PHH 123%20456"
360 ]
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")
364 self.client.logOut()
365
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")
373
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")
378
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')
382
383
384 #######################################
385 # Notification with fake server tests #
386 #######################################
387
388 class FakeNotificationServer(msn.MSNEventBase):
389 def handle_CHG(self, params):
390 if len(params) < 4:
391 params.append('')
392 self.sendLine("CHG %s %s %s %s" % (params[0], params[1], params[2], params[3]))
393
394 def handle_BLP(self, params):
395 self.sendLine("BLP %s %s 100" % (params[0], params[1]))
396
397 def handle_ADC(self, params):
398 trid = params[0]
399 list = msn.listCodeToID[params[1].lower()]
400 if list == msn.FORWARD_LIST:
401 userHandle = ""
402 screenName = ""
403 userGuid = ""
404 groups = ""
405 for p in params[2:]:
406 if p[0] == 'N':
407 userHandle = p[2:]
408 elif p[0] == 'F':
409 screenName = p[2:]
410 elif p[0] == 'C':
411 userGuid = p[2:]
412 else:
413 groups = p
414 if userHandle and userGuid:
415 self.transport.loseConnection()
416 return
417 if userHandle:
418 if not screenName:
419 self.transport.loseConnection()
420 else:
421 self.sendLine("ADC %s FL N=%s F=%s C=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx %s" % (trid, userHandle, screenName, groups))
422 return
423 if userGuid:
424 raise "NotImplementedError"
425 else:
426 if len(params) != 3:
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]))
431
432 def handle_REM(self, params):
433 if len(params) != 3:
434 self.transport.loseConnection()
435 return
436 try:
437 trid = int(params[0])
438 listType = msn.listCodeToID[params[1].lower()]
439 except:
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()
445 else:
446 self.sendLine("REM %s %s %s" % (params[0], params[1], params[2]))
447
448 def handle_PRP(self, params):
449 if len(params) != 3:
450 self.transport.loseConnection()
451 if params[1] == "MFN":
452 self.sendLine("PRP %s MFN %s" % (params[0], params[2]))
453 else:
454 # Only friendly names are implemented
455 self.transport.loseConnection()
456
457 def handle_UUX(self, params):
458 if len(params) != 2:
459 self.transport.loseConnection()
460 return
461 l = int(params[1])
462 if l > 0:
463 self.currentMessage = msn.MSNMessage(length=l, userHandle=params[0], screenName="UUX", specialMessage=True)
464 self.setRawMode()
465 else:
466 self.sendLine("UUX %s 0" % params[0])
467
468 def checkMessage(self, message):
469 if message.specialMessage:
470 if message.screenName == "UUX":
471 self.sendLine("UUX %s 0" % message.userHandle)
472 return 0
473 return 1
474
475 def handle_XFR(self, params):
476 if len(params) != 2:
477 self.transport.loseConnection()
478 return
479 if params[1] != "SB":
480 self.transport.loseConnection()
481 return
482 self.sendLine("XFR %s SB 129.129.129.129:1234 CKI SomeSecret" % params[0])
483
484
485
486 class FakeNotificationClient(msn.NotificationClient):
487 def doStatusChange(self):
488 def testcb((status,)):
489 if status == msn.STATUS_AWAY:
490 self.test = 'PASS'
491 self.transport.loseConnection()
492 d = self.changeStatus(msn.STATUS_AWAY)
493 d.addCallback(testcb)
494
495 def doPrivacyMode(self):
496 def testcb((priv,)):
497 if priv.upper() == 'AL':
498 self.test = 'PASS'
499 self.transport.loseConnection()
500 d = self.setPrivacyMode(True)
501 d.addCallback(testcb)
502
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":
509 self.test = 'PASS'
510 self.transport.loseConnection()
511 d = self.addContact(msn.FORWARD_LIST, "foo@bar.com")
512 d.addCallback(testcb)
513
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:
519 self.test = 'PASS'
520 self.transport.loseConnection()
521 d = self.addContact(msn.ALLOW_LIST, "foo@bar.com")
522 d.addCallback(testcb)
523
524 def doRemContactFL(self):
525 def testcb((listType, userHandle, groupID)):
526 if listType & msn.FORWARD_LIST and \
527 userHandle == "foo@bar.com":
528 self.test = 'PASS'
529 self.transport.loseConnection()
530 d = self.remContact(msn.FORWARD_LIST, "foo@bar.com")
531 d.addCallback(testcb)
532
533 def doRemContactAL(self):
534 def testcb((listType, userHandle, groupID)):
535 if listType & msn.ALLOW_LIST and \
536 userHandle == "foo@bar.com":
537 self.test = 'PASS'
538 self.transport.loseConnection()
539 d = self.remContact(msn.ALLOW_LIST, "foo@bar.com")
540 d.addCallback(testcb)
541
542 def doScreenNameChange(self):
543 def testcb((screenName,)):
544 if screenName == "Some new name":
545 self.test = 'PASS'
546 self.transport.loseConnection()
547 d = self.changeScreenName("Some new name")
548 d.addCallback(testcb)
549
550 def doPersonalChange(self, personal):
551 def testcb((checkPersonal,)):
552 if checkPersonal == personal:
553 self.test = 'PASS'
554 self.transport.loseConnection()
555 d = self.changePersonalMessage(personal)
556 d.addCallback(testcb)
557
558 def doAvatarChange(self, data):
559 def testcb(ignored):
560 self.test = 'PASS'
561 self.transport.loseConnection()
562 d = self.changeAvatar(data, True)
563 d.addCallback(testcb)
564
565 def doRequestSwitchboard(self):
566 def testcb((host, port, key)):
567 if host == "129.129.129.129" and port == 1234 and key == "SomeSecret":
568 self.test = 'PASS'
569 self.transport.loseConnection()
570 d = self.requestSwitchboardServer()
571 d.addCallback(testcb)
572
573 class FakeServerNotificationTests(unittest.TestCase):
574 """ tests the NotificationClient against a fake server. """
575
576 def setUp(self):
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)
582
583 def tearDown(self):
584 self.loop.disconnect()
585
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')
590
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')
596
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."
602
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')
608
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')
614
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')
621
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')
628
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')
633
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')
638
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')
643
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')
648
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')
653
654
655 #################################
656 # Notification challenges tests #
657 #################################
658
659 class DummyChallengeNotificationServer(msn.MSNEventBase):
660 def doChallenge(self, challenge, response):
661 self.state = 0
662 self.response = response
663 self.sendLine("CHL 0 " + challenge)
664
665 def checkMessage(self, message):
666 if message.message == self.response:
667 self.state = "PASS"
668 self.transport.loseConnection()
669 return 0
670
671 def handle_QRY(self, params):
672 self.state = 1
673 if len(params) == 3 and params[1] == "PROD0090YUAUV{2B" and params[2] == "32":
674 self.state = 2
675 self.currentMessage = msn.MSNMessage(length=32, userHandle="QRY", screenName="QRY", specialMessage=True)
676 self.setRawMode()
677 else:
678 self.transport.loseConnection()
679
680 class DummyChallengeNotificationClient(msn.NotificationClient):
681 def connectionMade(self):
682 msn.MSNEventBase.connectionMade(self)
683
684 def handle_CHL(self, params):
685 msn.NotificationClient.handle_CHL(self, params)
686 self.transport.loseConnection()
687
688
689 class NotificationChallengeTests(unittest.TestCase):
690 """ tests the responses to the CHLs the server sends """
691
692 def setUp(self):
693 self.client = DummyChallengeNotificationClient()
694 self.server = DummyChallengeNotificationServer()
695 self.loop = LoopbackCon(self.client, self.server)
696
697 def tearDown(self):
698 self.loop.disconnect()
699
700 def testChallenges(self):
701 challenges = [('13038318816579321232', 'b01c13020e374d4fa20abfad6981b7a9'),
702 ('23055170411503520698', 'ae906c3f2946d25e7da1b08b0b247659'),
703 ('37819769320541083311', 'db79d37dadd9031bef996893321da480'),
704 ('93662730714769834295', 'd619dfbb1414004d34d0628766636568')]
705 for challenge, response in challenges:
706 self.server.doChallenge(challenge, response)
707 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
708 self.failUnless((self.server.state == 'PASS'), 'Incorrect challenge response.')
709
710
711 ###########################
712 # Notification ping tests #
713 ###########################
714
715 class DummyPingNotificationServer(LineReceiver):
716 def lineReceived(self, line):
717 if line.startswith("PNG") and self.good:
718 self.sendLine("QNG 50")
719
720 class DummyPingNotificationClient(msn.NotificationClient):
721 def connectionMade(self):
722 self.pingCheckerStart()
723
724 def sendLine(self, line):
725 msn.NotificationClient.sendLine(self, line)
726 self.count += 1
727 if self.count > 10:
728 self.transport.loseConnection() # But not for real, just to end the test
729
730 def connectionLost(self, reason):
731 if self.count <= 10:
732 self.state = 'DISCONNECTED'
733
734 class NotificationPingTests(unittest.TestCase):
735 """ tests pinging in the NotificationClient class """
736
737 def setUp(self):
738 msn.PINGSPEED = 0.1
739 self.client = DummyPingNotificationClient()
740 self.server = DummyPingNotificationServer()
741 self.client.state = 'CONNECTED'
742 self.client.count = 0
743 self.loop = LoopbackCon(self.client, self.server)
744
745 def tearDown(self):
746 msn.PINGSPEED = 50.0
747 self.client.logOut()
748 self.loop.disconnect()
749
750 def testPingGood(self):
751 self.server.good = True
752 self.loop.doSteps(100)
753 self.failUnless((self.client.state == 'CONNECTED'), 'Should be connected.')
754
755 def testPingBad(self):
756 self.server.good = False
757 self.loop.doSteps(100)
758 self.failUnless((self.client.state == 'DISCONNECTED'), 'Should be disconnected.')
759
760
761
762
763 ###########################
764 # Switchboard basic tests #
765 ###########################
766
767 class DummySwitchboardServer(msn.MSNEventBase):
768 def handle_USR(self, params):
769 if len(params) != 3:
770 self.transport.loseConnection()
771 if params[1] == 'foo@bar.com' and params[2] == 'somekey':
772 self.sendLine("USR %s OK %s %s" % (params[0], params[1], params[1]))
773
774 def handle_ANS(self, params):
775 if len(params) != 4:
776 self.transport.loseConnection()
777 if params[1] == 'foo@bar.com' and params[2] == 'somekey' and params[3] == 'someSID':
778 self.sendLine("ANS %s OK" % params[0])
779
780 def handle_CAL(self, params):
781 if len(params) != 2:
782 self.transport.loseConnection()
783 if params[1] == 'friend@hotmail.com':
784 self.sendLine("CAL %s RINGING 1111122" % params[0])
785 else:
786 self.transport.loseConnection()
787
788 def checkMessage(self, message):
789 if message.message == 'Hi how are you today?':
790 self.sendLine("ACK " + message.userHandle) # Relies on TRID getting stored in userHandle trick
791 else:
792 self.transport.loseConnection()
793 return 0
794
795 class DummySwitchboardClient(msn.SwitchboardClient):
796 def loggedIn(self):
797 self.state = 'LOGGEDIN'
798 self.transport.loseConnection()
799
800 def gotChattingUsers(self, users):
801 if users == {'fred@hotmail.com': 'fred', 'jack@email.com': 'jack has a nickname!'}:
802 self.state = 'GOTCHATTINGUSERS'
803
804 def userJoined(self, userHandle, screenName):
805 if userHandle == "friend@hotmail.com" and screenName == "friend nickname":
806 self.state = 'USERJOINED'
807
808 def userLeft(self, userHandle):
809 if userHandle == "friend@hotmail.com":
810 self.state = 'USERLEFT'
811
812 def userTyping(self, message):
813 if message.userHandle == 'foo@bar.com':
814 self.state = 'USERTYPING'
815
816 def gotMessage(self, message):
817 if message.userHandle == 'friend@hotmail.com' and \
818 message.screenName == 'Friend Nickname' and \
819 message.message == 'Hello.':
820 self.state = 'GOTMESSAGE'
821
822 def doSendInvite(self):
823 def testcb((sid,)):
824 if sid == 1111122:
825 self.state = 'INVITESUCCESS'
826 self.transport.loseConnection()
827 d = self.inviteUser('friend@hotmail.com')
828 d.addCallback(testcb)
829
830 def doSendMessage(self):
831 def testcb(ignored):
832 self.state = 'MESSAGESUCCESS'
833 self.transport.loseConnection()
834 m = msn.MSNMessage()
835 m.setHeader("Content-Type", "text/plain; charset=UTF-8")
836 m.message = 'Hi how are you today?'
837 m.ack = msn.MSNMessage.MESSAGE_ACK
838 d = self.sendMessage(m)
839 d.addCallback(testcb)
840
841
842 class SwitchboardBasicTests(unittest.TestCase):
843 """ Tests basic functionality of switchboard sessions """
844 def setUp(self):
845 self.client = DummySwitchboardClient()
846 self.client.state = 'START'
847 self.client.userHandle = 'foo@bar.com'
848 self.client.key = 'somekey'
849 self.client.sessionID = 'someSID'
850 self.server = DummySwitchboardServer()
851 self.loop = LoopbackCon(self.client, self.server)
852
853 def tearDown(self):
854 self.loop.disconnect()
855
856 def _testSB(self, reply):
857 self.client.reply = reply
858 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
859 self.failUnless((self.client.state == 'LOGGEDIN'), 'Failed to login with reply='+str(reply))
860
861 def testReply(self):
862 self._testSB(True)
863
864 def testAsync(self):
865 self._testSB(False)
866
867 def testChattingUsers(self):
868 lines = ["IRO 1 1 2 fred@hotmail.com fred",
869 "IRO 1 2 2 jack@email.com jack%20has%20a%20nickname%21"]
870 for line in lines:
871 self.client.lineReceived(line)
872 self.failUnless((self.client.state == 'GOTCHATTINGUSERS'), 'Failed to get chatting users')
873
874 def testUserJoined(self):
875 self.client.lineReceived("JOI friend@hotmail.com friend%20nickname")
876 self.failUnless((self.client.state == 'USERJOINED'), 'Failed to notice user joining')
877
878 def testUserLeft(self):
879 self.client.lineReceived("BYE friend@hotmail.com")
880 self.failUnless((self.client.state == 'USERLEFT'), 'Failed to notice user leaving')
881
882 def testTypingCheck(self):
883 m = 'MSG foo@bar.com Foo 80\r\n'
884 m += 'MIME-Version: 1.0\r\n'
885 m += 'Content-Type: text/x-msmsgscontrol\r\n'
886 m += 'TypingUser: foo@bar\r\n'
887 m += '\r\n\r\n'
888 self.client.dataReceived(m)
889 self.failUnless((self.client.state == 'USERTYPING'), 'Failed to detect typing notification')
890
891 def testGotMessage(self):
892 m = 'MSG friend@hotmail.com Friend%20Nickname 68\r\n'
893 m += 'MIME-Version: 1.0\r\n'
894 m += 'Content-Type: text/plain; charset=UTF-8\r\n'
895 m += '\r\nHello.'
896 self.client.dataReceived(m)
897 self.failUnless((self.client.state == 'GOTMESSAGE'), 'Failed to detect message')
898
899 def testInviteUser(self):
900 self.client.connectionMade = lambda: None
901 self.client.doSendInvite()
902 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
903 self.failUnless((self.client.state == 'INVITESUCCESS'), 'Failed to invite user')
904
905 def testSendMessage(self):
906 self.client.connectionMade = lambda: None
907 self.client.doSendMessage()
908 self.failUnless(self.loop.doSteps(10), 'Failed to disconnect')
909 self.failUnless((self.client.state == 'MESSAGESUCCESS'), 'Failed to send message')
910
911
912 ################
913 # MSNP2P tests #
914 ################
915
916 class DummySwitchboardP2PServerHelper(msn.MSNEventBase):
917 def __init__(self, server):
918 msn.MSNEventBase.__init__(self)
919 self.server = server
920
921 def handle_USR(self, params):
922 if len(params) != 3:
923 self.transport.loseConnection()
924 self.userHandle = params[1]
925 if params[1] == 'foo1@bar.com' and params[2] == 'somekey1':
926 self.sendLine("USR %s OK %s %s" % (params[0], params[1], params[1]))
927 if params[1] == 'foo2@bar.com' and params[2] == 'somekey2':
928 self.sendLine("USR %s OK %s %s" % (params[0], params[1], params[1]))
929
930 def checkMessage(self, message):
931 return 1
932
933 def gotMessage(self, message):
934 message.userHandle = self.userHandle
935 message.screenName = self.userHandle
936 self.server.gotMessage(message, self)
937
938 def sendMessage(self, message):
939 if message.length == 0: message.length = message._calcMessageLen()
940 self.sendLine("MSG %s %s %s" % (message.userHandle, message.screenName, message.length))
941 self.sendLine('MIME-Version: %s' % message.getHeader('MIME-Version'))
942 self.sendLine('Content-Type: %s' % message.getHeader('Content-Type'))
943 for header in [h for h in message.headers.items() if h[0].lower() not in ('mime-version','content-type')]:
944 self.sendLine("%s: %s" % (header[0], header[1]))
945 self.transport.write("\r\n")
946 self.transport.write(message.message)
947
948
949 class DummySwitchboardP2PServer:
950 def __init__(self):
951 self.clients = []
952
953 def newClient(self):
954 c = DummySwitchboardP2PServerHelper(self)
955 self.clients.append(c)
956 return c
957
958 def gotMessage(self, message, sender):
959 for c in self.clients:
960 if c != sender:
961 c.sendMessage(message)
962
963 class DummySwitchboardP2PClient(msn.SwitchboardClient):
964 def gotMessage(self, message):
965 if message.message == "Test Message" and message.userHandle == "foo1@bar.com":
966 self.status = "GOTMESSAGE"
967
968 class SwitchboardP2PTests(unittest.TestCase):
969 def setUp(self):
970 self.server = DummySwitchboardP2PServer()
971 self.client1 = DummySwitchboardP2PClient()
972 self.client1.key = 'somekey1'
973 self.client1.userHandle = 'foo1@bar.com'
974 self.client2 = DummySwitchboardP2PClient()
975 self.client2.key = 'somekey2'
976 self.client2.userHandle = 'foo2@bar.com'
977 self.client2.status = "INIT"
978 self.loop1 = LoopbackCon(self.client1, self.server.newClient())
979 self.loop2 = LoopbackCon(self.client2, self.server.newClient())
980
981 def tearDown(self):
982 self.loop1.disconnect()
983 self.loop2.disconnect()
984
985 def testMessage(self):
986 self.client1.sendMessage(msn.MSNMessage(message='Test Message'))
987 self.loop1.doSteps(10)
988 self.loop2.doSteps(10)
989 self.failUnless((self.client2.status == "GOTMESSAGE"), "Fake switchboard server not working.")
990
991 def testAvatars(self):
992 self.gotAvatar = False
993
994 # Set up the avatar for client1
995 imageData = 'avatar image data' * 1000
996 self.client1.msnobj = msn.MSNObject()
997 self.client1.msnobj.setData('foo1@bar.com', imageData)
998 self.client1.msnobj.makeText()
999
1000 # Make client2 request the avatar
1001 def avatarCallback((data,)):
1002 self.gotAvatar = (data == imageData)
1003 msnContact = msn.MSNContact(userHandle='foo1@bar.com', msnobj=self.client1.msnobj)
1004 d = self.client2.sendAvatarRequest(msnContact)
1005 d.addCallback(avatarCallback)
1006
1007 # Let them do their thing
1008 for i in xrange(100):
1009 self.loop1.doSteps(1)
1010 self.loop2.doSteps(1)
1011
1012 # Check that client2 got the avatar
1013 self.failUnless((self.gotAvatar), "Failed to transfer avatar")
1014
1015
1016 ################
1017 # MSNFTP tests #
1018 ################
1019
1020 class FileTransferTestCase(unittest.TestCase):
1021 """ test FileSend against FileReceive """
1022
1023 def setUp(self):
1024 self.input = StringIOWithoutClosing()
1025 self.input.writelines(['a'] * 7000)
1026 self.input.seek(0)
1027 self.output = StringIOWithoutClosing()
1028
1029 def tearDown(self):
1030 self.input = None
1031 self.output = None
1032
1033 def testFileTransfer(self):
1034 auth = 1234
1035 sender = msnft.MSNFTP_FileSend(self.input)
1036 sender.auth = auth
1037 sender.fileSize = 7000
1038 client = msnft.MSNFTP_FileReceive(auth, "foo@bar.com", self.output)
1039 client.fileSize = 7000
1040 loop = LoopbackCon(client, sender)
1041 loop.doSteps(100)
1042 self.failUnless((client.completed and sender.completed), "send failed to complete")
1043 self.failUnless((self.input.getvalue() == self.output.getvalue()), "saved file does not match original")
1044
1045