1
# Copyright (c) 2001-2009 Twisted Matrix Laboratories.
2
# See LICENSE for details.
5
Test cases for L{twisted.words.protocols.msn}.
13
# t.w.p.msn requires an HTTP client
15
# So try to get one - do it directly instead of catching an ImportError
16
# from t.w.p.msn so that other problems which cause that module to fail
17
# to import don't cause the tests to be skipped.
18
from twisted.web import client
20
# If there isn't one, we're going to skip all the tests.
23
# Otherwise importing it should work, so do it.
24
from twisted.words.protocols import msn
27
from twisted.python.hashlib import md5
28
from twisted.protocols import loopback
29
from twisted.internet.defer import Deferred
30
from twisted.trial import unittest
31
from twisted.test.proto_helpers import StringTransport, StringIOWithoutClosing
37
class PassportTests(unittest.TestCase):
41
self.deferred = Deferred()
42
self.deferred.addCallback(lambda r: self.result.append(r))
43
self.deferred.addErrback(printError)
47
When L{msn.PassportNexus} receives enough information to identify the
48
address of the login server, it fires the L{Deferred} passed to its
49
initializer with that address.
51
protocol = msn.PassportNexus(self.deferred, 'https://foobar.com/somepage.quux')
53
'Content-Length' : '0',
54
'Content-Type' : 'text/html',
55
'PassportURLs' : 'DARealm=Passport.Net,DALogin=login.myserver.com/,DAReg=reg.myserver.com'
57
transport = StringTransport()
58
protocol.makeConnection(transport)
59
protocol.dataReceived('HTTP/1.0 200 OK\r\n')
60
for (h, v) in headers.items():
61
protocol.dataReceived('%s: %s\r\n' % (h,v))
62
protocol.dataReceived('\r\n')
63
self.assertEquals(self.result[0], "https://login.myserver.com/")
66
def _doLoginTest(self, response, headers):
67
protocol = msn.PassportLogin(self.deferred,'foo@foo.com','testpass','https://foo.com/', 'a')
68
protocol.makeConnection(StringTransport())
69
protocol.dataReceived(response)
70
for (h,v) in headers.items(): protocol.dataReceived('%s: %s\r\n' % (h,v))
71
protocol.dataReceived('\r\n')
73
def testPassportLoginSuccess(self):
75
'Content-Length' : '0',
76
'Content-Type' : 'text/html',
77
'Authentication-Info' : "Passport1.4 da-status=success,tname=MSPAuth," +
78
"tname=MSPProf,tname=MSPSec,from-PP='somekey'," +
79
"ru=http://messenger.msn.com"
81
self._doLoginTest('HTTP/1.1 200 OK\r\n', headers)
82
self.failUnless(self.result[0] == (msn.LOGIN_SUCCESS, 'somekey'))
84
def testPassportLoginFailure(self):
86
'Content-Type' : 'text/html',
87
'WWW-Authenticate' : 'Passport1.4 da-status=failed,' +
88
'srealm=Passport.NET,ts=-3,prompt,cburl=http://host.com,' +
89
'cbtxt=the%20error%20message'
91
self._doLoginTest('HTTP/1.1 401 Unauthorized\r\n', headers)
92
self.failUnless(self.result[0] == (msn.LOGIN_FAILURE, 'the error message'))
94
def testPassportLoginRedirect(self):
96
'Content-Type' : 'text/html',
97
'Authentication-Info' : 'Passport1.4 da-status=redir',
98
'Location' : 'https://newlogin.host.com/'
100
self._doLoginTest('HTTP/1.1 302 Found\r\n', headers)
101
self.failUnless(self.result[0] == (msn.LOGIN_REDIRECT, 'https://newlogin.host.com/', 'a'))
105
class DummySwitchboardClient(msn.SwitchboardClient):
106
def userTyping(self, message):
107
self.state = 'TYPING'
109
def gotSendRequest(self, fileName, fileSize, cookie, message):
110
if fileName == 'foobar.ext' and fileSize == 31337 and cookie == 1234: self.state = 'INVITATION'
113
class DummyNotificationClient(msn.NotificationClient):
114
def loggedIn(self, userHandle, screenName, verified):
115
if userHandle == 'foo@bar.com' and screenName == 'Test Screen Name' and verified:
118
def gotProfile(self, message):
119
self.state = 'PROFILE'
121
def gotContactStatus(self, code, userHandle, screenName):
122
if code == msn.STATUS_AWAY and userHandle == "foo@bar.com" and screenName == "Test Screen Name":
123
self.state = 'INITSTATUS'
125
def contactStatusChanged(self, code, userHandle, screenName):
126
if code == msn.STATUS_LUNCH and userHandle == "foo@bar.com" and screenName == "Test Name":
127
self.state = 'NEWSTATUS'
129
def contactOffline(self, userHandle):
130
if userHandle == "foo@bar.com": self.state = 'OFFLINE'
132
def statusChanged(self, code):
133
if code == msn.STATUS_HIDDEN: self.state = 'MYSTATUS'
135
def listSynchronized(self, *args):
136
self.state = 'GOTLIST'
138
def gotPhoneNumber(self, listVersion, userHandle, phoneType, number):
139
msn.NotificationClient.gotPhoneNumber(self, listVersion, userHandle, phoneType, number)
140
self.state = 'GOTPHONE'
142
def userRemovedMe(self, userHandle, listVersion):
143
msn.NotificationClient.userRemovedMe(self, userHandle, listVersion)
144
c = self.factory.contacts.getContact(userHandle)
145
if not c and self.factory.contacts.version == listVersion: self.state = 'USERREMOVEDME'
147
def userAddedMe(self, userHandle, screenName, listVersion):
148
msn.NotificationClient.userAddedMe(self, userHandle, screenName, listVersion)
149
c = self.factory.contacts.getContact(userHandle)
150
if c and (c.lists | msn.REVERSE_LIST) and (self.factory.contacts.version == listVersion) and \
151
(screenName == 'Screen Name'):
152
self.state = 'USERADDEDME'
154
def gotSwitchboardInvitation(self, sessionID, host, port, key, userHandle, screenName):
155
if sessionID == 1234 and \
156
host == '192.168.1.1' and \
158
key == '123.456' and \
159
userHandle == 'foo@foo.com' and \
160
screenName == 'Screen Name':
161
self.state = 'SBINVITED'
165
class DispatchTests(unittest.TestCase):
167
Tests for L{DispatchClient}.
169
def _versionTest(self, serverVersionResponse):
171
Test L{DispatchClient} version negotiation.
173
client = msn.DispatchClient()
174
client.userHandle = "foo"
176
transport = StringTransport()
177
client.makeConnection(transport)
179
transport.value(), "VER 1 MSNP8 CVR0\r\n")
182
client.dataReceived(serverVersionResponse)
185
"CVR 2 0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS foo\r\n")
188
def test_version(self):
190
L{DispatchClient.connectionMade} greets the server with a I{VER}
191
(version) message and then L{NotificationClient.dataReceived}
192
handles the server's I{VER} response by sending a I{CVR} (client
195
self._versionTest("VER 1 MSNP8 CVR0\r\n")
198
def test_versionWithoutCVR0(self):
200
If the server responds to a I{VER} command without including the
201
I{CVR0} protocol, L{DispatchClient} behaves in the same way as if
202
that protocol were included.
204
Starting in August 2008, CVR0 disappeared from the I{VER} response.
206
self._versionTest("VER 1 MSNP8\r\n")
210
class NotificationTests(unittest.TestCase):
211
""" testing the various events in NotificationClient """
214
self.client = DummyNotificationClient()
215
self.client.factory = msn.NotificationFactory()
216
self.client.state = 'START'
223
def _versionTest(self, serverVersionResponse):
225
Test L{NotificationClient} version negotiation.
227
self.client.factory.userHandle = "foo"
229
transport = StringTransport()
230
self.client.makeConnection(transport)
232
transport.value(), "VER 1 MSNP8 CVR0\r\n")
235
self.client.dataReceived(serverVersionResponse)
238
"CVR 2 0x0409 win 4.10 i386 MSNMSGR 5.0.0544 MSMSGS foo\r\n")
241
def test_version(self):
243
L{NotificationClient.connectionMade} greets the server with a I{VER}
244
(version) message and then L{NotificationClient.dataReceived}
245
handles the server's I{VER} response by sending a I{CVR} (client
248
self._versionTest("VER 1 MSNP8 CVR0\r\n")
251
def test_versionWithoutCVR0(self):
253
If the server responds to a I{VER} command without including the
254
I{CVR0} protocol, L{NotificationClient} behaves in the same way as
255
if that protocol were included.
257
Starting in August 2008, CVR0 disappeared from the I{VER} response.
259
self._versionTest("VER 1 MSNP8\r\n")
262
def test_challenge(self):
264
L{NotificationClient} responds to a I{CHL} message by sending a I{QRY}
265
back which included a hash based on the parameters of the I{CHL}.
267
transport = StringTransport()
268
self.client.makeConnection(transport)
271
challenge = "15570131571988941333"
272
self.client.dataReceived('CHL 0 ' + challenge + '\r\n')
273
# md5 of the challenge and a magic string defined by the protocol
274
response = "8f2f5a91b72102cd28355e9fc9000d6e"
275
# Sanity check - the response is what the comment above says it is.
277
response, md5(challenge + "Q1P7W2E4J9R8U3S5").hexdigest())
280
# 2 is the next transaction identifier. 32 is the length of the
282
"QRY 2 msmsgs@msnmsgr.com 32\r\n" + response)
286
self.client.lineReceived('USR 1 OK foo@bar.com Test%20Screen%20Name 1 0')
287
self.failUnless((self.client.state == 'LOGIN'), msg='Failed to detect successful login')
290
def testProfile(self):
291
m = 'MSG Hotmail Hotmail 353\r\nMIME-Version: 1.0\r\nContent-Type: text/x-msmsgsprofile; charset=UTF-8\r\n'
292
m += 'LoginTime: 1016941010\r\nEmailEnabled: 1\r\nMemberIdHigh: 40000\r\nMemberIdLow: -600000000\r\nlang_preference: 1033\r\n'
293
m += 'preferredEmail: foo@bar.com\r\ncountry: AU\r\nPostalCode: 90210\r\nGender: M\r\nKid: 0\r\nAge:\r\nsid: 400\r\n'
294
m += 'kv: 2\r\nMSPAuth: 2CACCBCCADMoV8ORoz64BVwmjtksIg!kmR!Rj5tBBqEaW9hc4YnPHSOQ$$\r\n\r\n'
295
map(self.client.lineReceived, m.split('\r\n')[:-1])
296
self.failUnless((self.client.state == 'PROFILE'), msg='Failed to detect initial profile')
298
def testStatus(self):
299
t = [('ILN 1 AWY foo@bar.com Test%20Screen%20Name 0', 'INITSTATUS', 'Failed to detect initial status report'),
300
('NLN LUN foo@bar.com Test%20Name 0', 'NEWSTATUS', 'Failed to detect contact status change'),
301
('FLN foo@bar.com', 'OFFLINE', 'Failed to detect contact signing off'),
302
('CHG 1 HDN 0', 'MYSTATUS', 'Failed to detect my status changing')]
304
self.client.lineReceived(i[0])
305
self.failUnless((self.client.state == i[1]), msg=i[2])
307
def testListSync(self):
308
# currently this test does not take into account the fact
309
# that BPRs sent as part of the SYN reply may not be interpreted
310
# as such if they are for the last LST -- maybe I should
311
# factor this in later.
312
self.client.makeConnection(StringTransport())
313
msn.NotificationClient.loggedIn(self.client, 'foo@foo.com', 'foobar', 1)
315
"SYN %s 100 1 1" % self.client.currentID,
318
"LSG 0 Other%20Contacts 0",
319
"LST userHandle@email.com Some%20Name 11 0"
321
map(self.client.lineReceived, lines)
322
contacts = self.client.factory.contacts
323
contact = contacts.getContact('userHandle@email.com')
324
self.failUnless(contacts.version == 100, "Invalid contact list version")
325
self.failUnless(contact.screenName == 'Some Name', "Invalid screen-name for user")
326
self.failUnless(contacts.groups == {0 : 'Other Contacts'}, "Did not get proper group list")
327
self.failUnless(contact.groups == [0] and contact.lists == 11, "Invalid contact list/group info")
328
self.failUnless(self.client.state == 'GOTLIST', "Failed to call list sync handler")
330
def testAsyncPhoneChange(self):
331
c = msn.MSNContact(userHandle='userHandle@email.com')
332
self.client.factory.contacts = msn.MSNContactList()
333
self.client.factory.contacts.addContact(c)
334
self.client.makeConnection(StringTransport())
335
self.client.lineReceived("BPR 101 userHandle@email.com PHH 123%20456")
336
c = self.client.factory.contacts.getContact('userHandle@email.com')
337
self.failUnless(self.client.state == 'GOTPHONE', "Did not fire phone change callback")
338
self.failUnless(c.homePhone == '123 456', "Did not update the contact's phone number")
339
self.failUnless(self.client.factory.contacts.version == 101, "Did not update list version")
341
def testLateBPR(self):
343
This test makes sure that if a BPR response that was meant
344
to be part of a SYN response (but came after the last LST)
345
is received, the correct contact is updated and all is well
347
self.client.makeConnection(StringTransport())
348
msn.NotificationClient.loggedIn(self.client, 'foo@foo.com', 'foo', 1)
350
"SYN %s 100 1 1" % self.client.currentID,
353
"LSG 0 Other%20Contacts 0",
354
"LST userHandle@email.com Some%20Name 11 0",
357
map(self.client.lineReceived, lines)
358
contact = self.client.factory.contacts.getContact('userHandle@email.com')
359
self.failUnless(contact.homePhone == '123 456', "Did not update contact's phone number")
361
def testUserRemovedMe(self):
362
self.client.factory.contacts = msn.MSNContactList()
363
contact = msn.MSNContact(userHandle='foo@foo.com')
364
contact.addToList(msn.REVERSE_LIST)
365
self.client.factory.contacts.addContact(contact)
366
self.client.lineReceived("REM 0 RL 100 foo@foo.com")
367
self.failUnless(self.client.state == 'USERREMOVEDME', "Failed to remove user from reverse list")
369
def testUserAddedMe(self):
370
self.client.factory.contacts = msn.MSNContactList()
371
self.client.lineReceived("ADD 0 RL 100 foo@foo.com Screen%20Name")
372
self.failUnless(self.client.state == 'USERADDEDME', "Failed to add user to reverse lise")
374
def testAsyncSwitchboardInvitation(self):
375
self.client.lineReceived("RNG 1234 192.168.1.1:1863 CKI 123.456 foo@foo.com Screen%20Name")
376
self.failUnless(self.client.state == "SBINVITED")
378
def testCommandFailed(self):
380
Ensures that error responses from the server fires an errback with
383
id, d = self.client._createIDMapping()
384
self.client.lineReceived("201 %s" % id)
385
d = self.assertFailure(d, msn.MSNCommandFailed)
386
def assertErrorCode(exception):
387
self.assertEqual(201, exception.errorCode)
388
return d.addCallback(assertErrorCode)
391
class MessageHandlingTests(unittest.TestCase):
392
""" testing various message handling methods from SwichboardClient """
395
self.client = DummySwitchboardClient()
396
self.client.state = 'START'
401
def testClientCapabilitiesCheck(self):
403
m.setHeader('Content-Type', 'text/x-clientcaps')
404
self.assertEquals(self.client.checkMessage(m), 0, 'Failed to detect client capability message')
406
def testTypingCheck(self):
408
m.setHeader('Content-Type', 'text/x-msmsgscontrol')
409
m.setHeader('TypingUser', 'foo@bar')
410
self.client.checkMessage(m)
411
self.failUnless((self.client.state == 'TYPING'), msg='Failed to detect typing notification')
413
def testFileInvitation(self, lazyClient=False):
415
m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
416
m.message += 'Application-Name: File Transfer\r\n'
418
m.message += 'Application-GUID: {5D3E02AB-6190-11d3-BBBB-00C04F795683}\r\n'
419
m.message += 'Invitation-Command: Invite\r\n'
420
m.message += 'Invitation-Cookie: 1234\r\n'
421
m.message += 'Application-File: foobar.ext\r\n'
422
m.message += 'Application-FileSize: 31337\r\n\r\n'
423
self.client.checkMessage(m)
424
self.failUnless((self.client.state == 'INVITATION'), msg='Failed to detect file transfer invitation')
426
def testFileInvitationMissingGUID(self):
427
return self.testFileInvitation(True)
429
def testFileResponse(self):
431
d.addCallback(self.fileResponse)
432
self.client.cookies['iCookies'][1234] = (d, None)
434
m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
435
m.message += 'Invitation-Command: ACCEPT\r\n'
436
m.message += 'Invitation-Cookie: 1234\r\n\r\n'
437
self.client.checkMessage(m)
438
self.failUnless((self.client.state == 'RESPONSE'), msg='Failed to detect file transfer response')
440
def testFileInfo(self):
442
d.addCallback(self.fileInfo)
443
self.client.cookies['external'][1234] = (d, None)
445
m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8')
446
m.message += 'Invitation-Command: ACCEPT\r\n'
447
m.message += 'Invitation-Cookie: 1234\r\n'
448
m.message += 'IP-Address: 192.168.0.1\r\n'
449
m.message += 'Port: 6891\r\n'
450
m.message += 'AuthCookie: 4321\r\n\r\n'
451
self.client.checkMessage(m)
452
self.failUnless((self.client.state == 'INFO'), msg='Failed to detect file transfer info')
454
def fileResponse(self, (accept, cookie, info)):
455
if accept and cookie == 1234: self.client.state = 'RESPONSE'
457
def fileInfo(self, (accept, ip, port, aCookie, info)):
458
if accept and ip == '192.168.0.1' and port == 6891 and aCookie == 4321: self.client.state = 'INFO'
461
class FileTransferTestCase(unittest.TestCase):
463
test FileSend against FileReceive
467
self.input = 'a' * 7000
468
self.output = StringIOWithoutClosing()
476
def test_fileTransfer(self):
478
Test L{FileSend} against L{FileReceive} using a loopback transport.
481
sender = msn.FileSend(StringIO.StringIO(self.input))
483
sender.fileSize = 7000
484
client = msn.FileReceive(auth, "foo@bar.com", self.output)
485
client.fileSize = 7000
488
client.completed and sender.completed,
489
msg="send failed to complete")
491
self.input, self.output.getvalue(),
492
msg="saved file does not match original")
493
d = loopback.loopbackAsync(sender, client)
499
for testClass in [PassportTests, NotificationTests,
500
MessageHandlingTests, FileTransferTestCase]:
502
"MSN requires an HTTP client but none is available, "