OLD | NEW |
| (Empty) |
1 # Copyright (c) 2001-2005 Twisted Matrix Laboratories. | |
2 # See LICENSE for details. | |
3 | |
4 """ | |
5 Test cases for twisted.words.protocols.msn | |
6 """ | |
7 | |
8 # System imports | |
9 import StringIO, sys | |
10 | |
11 # Twisted imports | |
12 | |
13 # t.w.p.msn requires an HTTP client | |
14 try: | |
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 | |
19 except ImportError: | |
20 # If there isn't one, we're going to skip all the tests. | |
21 msn = None | |
22 else: | |
23 # Otherwise importing it should work, so do it. | |
24 from twisted.words.protocols import msn | |
25 | |
26 | |
27 from twisted.protocols import loopback | |
28 from twisted.internet.defer import Deferred | |
29 from twisted.trial import unittest | |
30 | |
31 def printError(f): | |
32 print f | |
33 | |
34 class StringIOWithoutClosing(StringIO.StringIO): | |
35 disconnecting = 0 | |
36 def close(self): pass | |
37 def loseConnection(self): pass | |
38 | |
39 class PassportTests(unittest.TestCase): | |
40 | |
41 def setUp(self): | |
42 self.result = [] | |
43 self.deferred = Deferred() | |
44 self.deferred.addCallback(lambda r: self.result.append(r)) | |
45 self.deferred.addErrback(printError) | |
46 | |
47 def testNexus(self): | |
48 protocol = msn.PassportNexus(self.deferred, 'https://foobar.com/somepage
.quux') | |
49 headers = { | |
50 'Content-Length' : '0', | |
51 'Content-Type' : 'text/html', | |
52 'PassportURLs' : 'DARealm=Passport.Net,DALogin=login.myserver.com/
,DAReg=reg.myserver.com' | |
53 } | |
54 transport = StringIOWithoutClosing() | |
55 protocol.makeConnection(transport) | |
56 protocol.dataReceived('HTTP/1.0 200 OK\r\n') | |
57 for (h,v) in headers.items(): protocol.dataReceived('%s: %s\r\n' % (h,v)
) | |
58 protocol.dataReceived('\r\n') | |
59 self.failUnless(self.result[0] == "https://login.myserver.com/") | |
60 | |
61 def _doLoginTest(self, response, headers): | |
62 protocol = msn.PassportLogin(self.deferred,'foo@foo.com','testpass','htt
ps://foo.com/', 'a') | |
63 protocol.makeConnection(StringIOWithoutClosing()) | |
64 protocol.dataReceived(response) | |
65 for (h,v) in headers.items(): protocol.dataReceived('%s: %s\r\n' % (h,v)
) | |
66 protocol.dataReceived('\r\n') | |
67 | |
68 def testPassportLoginSuccess(self): | |
69 headers = { | |
70 'Content-Length' : '0', | |
71 'Content-Type' : 'text/html', | |
72 'Authentication-Info' : "Passport1.4 da-status=success,tname=MSPAuth
," + | |
73 "tname=MSPProf,tname=MSPSec,from-PP='somekey
'," + | |
74 "ru=http://messenger.msn.com" | |
75 } | |
76 self._doLoginTest('HTTP/1.1 200 OK\r\n', headers) | |
77 self.failUnless(self.result[0] == (msn.LOGIN_SUCCESS, 'somekey')) | |
78 | |
79 def testPassportLoginFailure(self): | |
80 headers = { | |
81 'Content-Type' : 'text/html', | |
82 'WWW-Authenticate' : 'Passport1.4 da-status=failed,' + | |
83 'srealm=Passport.NET,ts=-3,prompt,cburl=http://
host.com,' + | |
84 'cbtxt=the%20error%20message' | |
85 } | |
86 self._doLoginTest('HTTP/1.1 401 Unauthorized\r\n', headers) | |
87 self.failUnless(self.result[0] == (msn.LOGIN_FAILURE, 'the error message
')) | |
88 | |
89 def testPassportLoginRedirect(self): | |
90 headers = { | |
91 'Content-Type' : 'text/html', | |
92 'Authentication-Info' : 'Passport1.4 da-status=redir', | |
93 'Location' : 'https://newlogin.host.com/' | |
94 } | |
95 self._doLoginTest('HTTP/1.1 302 Found\r\n', headers) | |
96 self.failUnless(self.result[0] == (msn.LOGIN_REDIRECT, 'https://newlogin
.host.com/', 'a')) | |
97 | |
98 | |
99 if msn is not None: | |
100 class DummySwitchboardClient(msn.SwitchboardClient): | |
101 def userTyping(self, message): | |
102 self.state = 'TYPING' | |
103 | |
104 def gotSendRequest(self, fileName, fileSize, cookie, message): | |
105 if fileName == 'foobar.ext' and fileSize == 31337 and cookie == 1234
: self.state = 'INVITATION' | |
106 | |
107 | |
108 class DummyNotificationClient(msn.NotificationClient): | |
109 def loggedIn(self, userHandle, screenName, verified): | |
110 if userHandle == 'foo@bar.com' and screenName == 'Test Screen Name'
and verified: | |
111 self.state = 'LOGIN' | |
112 | |
113 def gotProfile(self, message): | |
114 self.state = 'PROFILE' | |
115 | |
116 def gotContactStatus(self, code, userHandle, screenName): | |
117 if code == msn.STATUS_AWAY and userHandle == "foo@bar.com" and scree
nName == "Test Screen Name": | |
118 self.state = 'INITSTATUS' | |
119 | |
120 def contactStatusChanged(self, code, userHandle, screenName): | |
121 if code == msn.STATUS_LUNCH and userHandle == "foo@bar.com" and scre
enName == "Test Name": | |
122 self.state = 'NEWSTATUS' | |
123 | |
124 def contactOffline(self, userHandle): | |
125 if userHandle == "foo@bar.com": self.state = 'OFFLINE' | |
126 | |
127 def statusChanged(self, code): | |
128 if code == msn.STATUS_HIDDEN: self.state = 'MYSTATUS' | |
129 | |
130 def listSynchronized(self, *args): | |
131 self.state = 'GOTLIST' | |
132 | |
133 def gotPhoneNumber(self, listVersion, userHandle, phoneType, number): | |
134 msn.NotificationClient.gotPhoneNumber(self, listVersion, userHandle,
phoneType, number) | |
135 self.state = 'GOTPHONE' | |
136 | |
137 def userRemovedMe(self, userHandle, listVersion): | |
138 msn.NotificationClient.userRemovedMe(self, userHandle, listVersion) | |
139 c = self.factory.contacts.getContact(userHandle) | |
140 if not c and self.factory.contacts.version == listVersion: self.stat
e = 'USERREMOVEDME' | |
141 | |
142 def userAddedMe(self, userHandle, screenName, listVersion): | |
143 msn.NotificationClient.userAddedMe(self, userHandle, screenName, lis
tVersion) | |
144 c = self.factory.contacts.getContact(userHandle) | |
145 if c and (c.lists | msn.REVERSE_LIST) and (self.factory.contacts.ver
sion == listVersion) and \ | |
146 (screenName == 'Screen Name'): | |
147 self.state = 'USERADDEDME' | |
148 | |
149 def gotSwitchboardInvitation(self, sessionID, host, port, key, userHandl
e, screenName): | |
150 if sessionID == 1234 and \ | |
151 host == '192.168.1.1' and \ | |
152 port == 1863 and \ | |
153 key == '123.456' and \ | |
154 userHandle == 'foo@foo.com' and \ | |
155 screenName == 'Screen Name': | |
156 self.state = 'SBINVITED' | |
157 | |
158 class NotificationTests(unittest.TestCase): | |
159 """ testing the various events in NotificationClient """ | |
160 | |
161 def setUp(self): | |
162 self.client = DummyNotificationClient() | |
163 self.client.factory = msn.NotificationFactory() | |
164 self.client.state = 'START' | |
165 | |
166 def tearDown(self): | |
167 self.client = None | |
168 | |
169 def testLogin(self): | |
170 self.client.lineReceived('USR 1 OK foo@bar.com Test%20Screen%20Name 1 0'
) | |
171 self.failUnless((self.client.state == 'LOGIN'), msg='Failed to detect su
ccessful login') | |
172 | |
173 def testProfile(self): | |
174 m = 'MSG Hotmail Hotmail 353\r\nMIME-Version: 1.0\r\nContent-Type: text/
x-msmsgsprofile; charset=UTF-8\r\n' | |
175 m += 'LoginTime: 1016941010\r\nEmailEnabled: 1\r\nMemberIdHigh: 40000\r\
nMemberIdLow: -600000000\r\nlang_preference: 1033\r\n' | |
176 m += 'preferredEmail: foo@bar.com\r\ncountry: AU\r\nPostalCode: 90210\r\
nGender: M\r\nKid: 0\r\nAge:\r\nsid: 400\r\n' | |
177 m += 'kv: 2\r\nMSPAuth: 2CACCBCCADMoV8ORoz64BVwmjtksIg!kmR!Rj5tBBqEaW9hc
4YnPHSOQ$$\r\n\r\n' | |
178 map(self.client.lineReceived, m.split('\r\n')[:-1]) | |
179 self.failUnless((self.client.state == 'PROFILE'), msg='Failed to detect
initial profile') | |
180 | |
181 def testStatus(self): | |
182 t = [('ILN 1 AWY foo@bar.com Test%20Screen%20Name 0', 'INITSTATUS', 'Fai
led to detect initial status report'), | |
183 ('NLN LUN foo@bar.com Test%20Name 0', 'NEWSTATUS', 'Failed to detec
t contact status change'), | |
184 ('FLN foo@bar.com', 'OFFLINE', 'Failed to detect contact signing of
f'), | |
185 ('CHG 1 HDN 0', 'MYSTATUS', 'Failed to detect my status changing')] | |
186 for i in t: | |
187 self.client.lineReceived(i[0]) | |
188 self.failUnless((self.client.state == i[1]), msg=i[2]) | |
189 | |
190 def testListSync(self): | |
191 # currently this test does not take into account the fact | |
192 # that BPRs sent as part of the SYN reply may not be interpreted | |
193 # as such if they are for the last LST -- maybe I should | |
194 # factor this in later. | |
195 self.client.makeConnection(StringIOWithoutClosing()) | |
196 msn.NotificationClient.loggedIn(self.client, 'foo@foo.com', 'foobar', 1) | |
197 lines = [ | |
198 "SYN %s 100 1 1" % self.client.currentID, | |
199 "GTC A", | |
200 "BLP AL", | |
201 "LSG 0 Other%20Contacts 0", | |
202 "LST userHandle@email.com Some%20Name 11 0" | |
203 ] | |
204 map(self.client.lineReceived, lines) | |
205 contacts = self.client.factory.contacts | |
206 contact = contacts.getContact('userHandle@email.com') | |
207 self.failUnless(contacts.version == 100, "Invalid contact list version") | |
208 self.failUnless(contact.screenName == 'Some Name', "Invalid screen-name
for user") | |
209 self.failUnless(contacts.groups == {0 : 'Other Contacts'}, "Did not get
proper group list") | |
210 self.failUnless(contact.groups == [0] and contact.lists == 11, "Invalid
contact list/group info") | |
211 self.failUnless(self.client.state == 'GOTLIST', "Failed to call list syn
c handler") | |
212 | |
213 def testAsyncPhoneChange(self): | |
214 c = msn.MSNContact(userHandle='userHandle@email.com') | |
215 self.client.factory.contacts = msn.MSNContactList() | |
216 self.client.factory.contacts.addContact(c) | |
217 self.client.makeConnection(StringIOWithoutClosing()) | |
218 self.client.lineReceived("BPR 101 userHandle@email.com PHH 123%20456") | |
219 c = self.client.factory.contacts.getContact('userHandle@email.com') | |
220 self.failUnless(self.client.state == 'GOTPHONE', "Did not fire phone cha
nge callback") | |
221 self.failUnless(c.homePhone == '123 456', "Did not update the contact's
phone number") | |
222 self.failUnless(self.client.factory.contacts.version == 101, "Did not up
date list version") | |
223 | |
224 def testLateBPR(self): | |
225 """ | |
226 This test makes sure that if a BPR response that was meant | |
227 to be part of a SYN response (but came after the last LST) | |
228 is received, the correct contact is updated and all is well | |
229 """ | |
230 self.client.makeConnection(StringIOWithoutClosing()) | |
231 msn.NotificationClient.loggedIn(self.client, 'foo@foo.com', 'foo', 1) | |
232 lines = [ | |
233 "SYN %s 100 1 1" % self.client.currentID, | |
234 "GTC A", | |
235 "BLP AL", | |
236 "LSG 0 Other%20Contacts 0", | |
237 "LST userHandle@email.com Some%20Name 11 0", | |
238 "BPR PHH 123%20456" | |
239 ] | |
240 map(self.client.lineReceived, lines) | |
241 contact = self.client.factory.contacts.getContact('userHandle@email.com'
) | |
242 self.failUnless(contact.homePhone == '123 456', "Did not update contact'
s phone number") | |
243 | |
244 def testUserRemovedMe(self): | |
245 self.client.factory.contacts = msn.MSNContactList() | |
246 contact = msn.MSNContact(userHandle='foo@foo.com') | |
247 contact.addToList(msn.REVERSE_LIST) | |
248 self.client.factory.contacts.addContact(contact) | |
249 self.client.lineReceived("REM 0 RL 100 foo@foo.com") | |
250 self.failUnless(self.client.state == 'USERREMOVEDME', "Failed to remove
user from reverse list") | |
251 | |
252 def testUserAddedMe(self): | |
253 self.client.factory.contacts = msn.MSNContactList() | |
254 self.client.lineReceived("ADD 0 RL 100 foo@foo.com Screen%20Name") | |
255 self.failUnless(self.client.state == 'USERADDEDME', "Failed to add user
to reverse lise") | |
256 | |
257 def testAsyncSwitchboardInvitation(self): | |
258 self.client.lineReceived("RNG 1234 192.168.1.1:1863 CKI 123.456 foo@foo.
com Screen%20Name") | |
259 self.failUnless(self.client.state == "SBINVITED") | |
260 | |
261 def testCommandFailed(self): | |
262 """ | |
263 Ensures that error responses from the server fires an errback with | |
264 MSNCommandFailed. | |
265 """ | |
266 id, d = self.client._createIDMapping() | |
267 self.client.lineReceived("201 %s" % id) | |
268 d = self.assertFailure(d, msn.MSNCommandFailed) | |
269 def assertErrorCode(exception): | |
270 self.assertEqual(201, exception.errorCode) | |
271 return d.addCallback(assertErrorCode) | |
272 | |
273 | |
274 class MessageHandlingTests(unittest.TestCase): | |
275 """ testing various message handling methods from SwichboardClient """ | |
276 | |
277 def setUp(self): | |
278 self.client = DummySwitchboardClient() | |
279 self.client.state = 'START' | |
280 | |
281 def tearDown(self): | |
282 self.client = None | |
283 | |
284 def testClientCapabilitiesCheck(self): | |
285 m = msn.MSNMessage() | |
286 m.setHeader('Content-Type', 'text/x-clientcaps') | |
287 self.assertEquals(self.client.checkMessage(m), 0, 'Failed to detect clie
nt capability message') | |
288 | |
289 def testTypingCheck(self): | |
290 m = msn.MSNMessage() | |
291 m.setHeader('Content-Type', 'text/x-msmsgscontrol') | |
292 m.setHeader('TypingUser', 'foo@bar') | |
293 self.client.checkMessage(m) | |
294 self.failUnless((self.client.state == 'TYPING'), msg='Failed to detect t
yping notification') | |
295 | |
296 def testFileInvitation(self, lazyClient=False): | |
297 m = msn.MSNMessage() | |
298 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8') | |
299 m.message += 'Application-Name: File Transfer\r\n' | |
300 if not lazyClient: | |
301 m.message += 'Application-GUID: {5D3E02AB-6190-11d3-BBBB-00C04F79568
3}\r\n' | |
302 m.message += 'Invitation-Command: Invite\r\n' | |
303 m.message += 'Invitation-Cookie: 1234\r\n' | |
304 m.message += 'Application-File: foobar.ext\r\n' | |
305 m.message += 'Application-FileSize: 31337\r\n\r\n' | |
306 self.client.checkMessage(m) | |
307 self.failUnless((self.client.state == 'INVITATION'), msg='Failed to dete
ct file transfer invitation') | |
308 | |
309 def testFileInvitationMissingGUID(self): | |
310 return self.testFileInvitation(True) | |
311 | |
312 def testFileResponse(self): | |
313 d = Deferred() | |
314 d.addCallback(self.fileResponse) | |
315 self.client.cookies['iCookies'][1234] = (d, None) | |
316 m = msn.MSNMessage() | |
317 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8') | |
318 m.message += 'Invitation-Command: ACCEPT\r\n' | |
319 m.message += 'Invitation-Cookie: 1234\r\n\r\n' | |
320 self.client.checkMessage(m) | |
321 self.failUnless((self.client.state == 'RESPONSE'), msg='Failed to detect
file transfer response') | |
322 | |
323 def testFileInfo(self): | |
324 d = Deferred() | |
325 d.addCallback(self.fileInfo) | |
326 self.client.cookies['external'][1234] = (d, None) | |
327 m = msn.MSNMessage() | |
328 m.setHeader('Content-Type', 'text/x-msmsgsinvite; charset=UTF-8') | |
329 m.message += 'Invitation-Command: ACCEPT\r\n' | |
330 m.message += 'Invitation-Cookie: 1234\r\n' | |
331 m.message += 'IP-Address: 192.168.0.1\r\n' | |
332 m.message += 'Port: 6891\r\n' | |
333 m.message += 'AuthCookie: 4321\r\n\r\n' | |
334 self.client.checkMessage(m) | |
335 self.failUnless((self.client.state == 'INFO'), msg='Failed to detect fil
e transfer info') | |
336 | |
337 def fileResponse(self, (accept, cookie, info)): | |
338 if accept and cookie == 1234: self.client.state = 'RESPONSE' | |
339 | |
340 def fileInfo(self, (accept, ip, port, aCookie, info)): | |
341 if accept and ip == '192.168.0.1' and port == 6891 and aCookie == 4321:
self.client.state = 'INFO' | |
342 | |
343 | |
344 class FileTransferTestCase(unittest.TestCase): | |
345 """ test FileSend against FileReceive """ | |
346 | |
347 def setUp(self): | |
348 self.input = StringIOWithoutClosing() | |
349 self.input.writelines(['a'] * 7000) | |
350 self.input.seek(0) | |
351 self.output = StringIOWithoutClosing() | |
352 | |
353 def tearDown(self): | |
354 self.input = None | |
355 self.output = None | |
356 | |
357 def testFileTransfer(self): | |
358 auth = 1234 | |
359 sender = msn.FileSend(self.input) | |
360 sender.auth = auth | |
361 sender.fileSize = 7000 | |
362 client = msn.FileReceive(auth, "foo@bar.com", self.output) | |
363 client.fileSize = 7000 | |
364 def check(ignored): | |
365 self.failUnless((client.completed and sender.completed), | |
366 msg="send failed to complete") | |
367 self.failUnless((self.input.getvalue() == self.output.getvalue()), | |
368 msg="saved file does not match original") | |
369 d = loopback.loopbackAsync(sender, client) | |
370 d.addCallback(check) | |
371 return d | |
372 | |
373 | |
374 if msn is None: | |
375 for testClass in [PassportTests, NotificationTests, | |
376 MessageHandlingTests, FileTransferTestCase]: | |
377 testClass.skip = ( | |
378 "MSN requires an HTTP client but none is available, " | |
379 "skipping tests.") | |
OLD | NEW |