source: wokkel/test/test_muc.py @ 146:14d3e8f9aa45

wokkel-muc-client-support-24
Last change on this file since 146:14d3e8f9aa45 was 146:14d3e8f9aa45, checked in by Ralph Meijer <ralphm@…>, 9 years ago

Add test for room history limit.

File size: 24.1 KB
Line 
1# Copyright (c) Ralph Meijer.
2# See LICENSE for details.
3
4"""
5Tests for L{wokkel.muc}
6"""
7
8from datetime import datetime
9from dateutil.tz import tzutc
10
11from zope.interface import verify
12
13from twisted.trial import unittest
14from twisted.internet import defer, task
15from twisted.words.xish import domish, xpath
16from twisted.words.protocols.jabber.jid import JID
17from twisted.words.protocols.jabber.error import StanzaError
18from twisted.words.protocols.jabber.xmlstream import TimeoutError, toResponse
19
20from wokkel import data_form, iwokkel, muc
21from wokkel.generic import parseXml
22from wokkel.test.helpers import XmlStreamStub
23
24
25NS_MUC_ADMIN = 'http://jabber.org/protocol/muc#admin'
26
27def calledAsync(fn):
28    """
29    Function wrapper that fires a deferred upon calling the given function.
30    """
31    d = defer.Deferred()
32
33    def func(*args, **kwargs):
34        try:
35            result = fn(*args, **kwargs)
36        except:
37            d.errback()
38        else:
39            d.callback(result)
40
41    return d, func
42
43
44
45class MUCClientTest(unittest.TestCase):
46    timeout = 2
47
48    def setUp(self):
49        self.stub = XmlStreamStub()
50        self.clock = task.Clock()
51        self.protocol = muc.MUCClient(reactor=self.clock)
52        self.protocol.xmlstream = self.stub.xmlstream
53        self.protocol.connectionInitialized()
54        self.roomIdentifier = 'test'
55        self.service  = 'conference.example.org'
56        self.nick = 'Nick'
57
58        self.occupantJID = JID(tuple=(self.roomIdentifier,
59                                      self.service,
60                                      self.nick))
61        self.roomJID = self.occupantJID.userhostJID()
62        self.userJID = JID('test@example.org/Testing')
63
64
65    def _createRoom(self):
66        """
67        A helper method to create a test room.
68        """
69        # create a room
70        room = muc.Room(self.roomIdentifier,
71                        self.service,
72                        self.nick)
73        self.protocol._addRoom(room)
74
75
76    def test_interface(self):
77        """
78        Do instances of L{muc.MUCClient} provide L{iwokkel.IMUCClient}?
79        """
80        verify.verifyObject(iwokkel.IMUCClient, self.protocol)
81
82
83    def test_userJoinedRoom(self):
84        """
85        Joins by others to a room we're in are passed to userJoinedRoom
86        """
87        xml = """
88            <presence to='%s' from='%s'>
89              <x xmlns='http://jabber.org/protocol/muc#user'>
90                <item affiliation='member' role='participant'/>
91              </x>
92            </presence>
93        """ % (self.userJID.full(), self.occupantJID.full())
94
95        # create a room
96        self._createRoom()
97
98        def userJoinedRoom(room, user):
99            self.assertEquals(self.roomIdentifier, room.roomIdentifier,
100                              'Wrong room name')
101            self.assertTrue(room.inRoster(user), 'User not in roster')
102
103        d, self.protocol.userJoinedRoom = calledAsync(userJoinedRoom)
104        self.stub.send(parseXml(xml))
105        return d
106
107
108    def test_receivedSubject(self):
109        """
110        Subject received from a room we're in are passed to receivedSubject.
111        """
112        xml = u"""
113            <message to='%s' from='%s' type='groupchat'>
114              <subject>test</subject>
115            </message>
116        """ % (self.userJID, self.occupantJID)
117
118        self._createRoom()
119
120        # add user to room
121        user = muc.User(self.nick)
122        room = self.protocol._getRoom(self.roomJID)
123        room.addUser(user)
124
125        def receivedSubject(room, user, subject):
126            self.assertEquals('test', subject, "Wrong group chat message")
127            self.assertEquals(self.roomIdentifier, room.roomIdentifier,
128                              'Wrong room name')
129            self.assertEquals(self.nick, user.nick)
130
131        d, self.protocol.receivedSubject = calledAsync(receivedSubject)
132        self.stub.send(parseXml(xml))
133        return d
134
135
136    def test_receivedGroupChat(self):
137        """
138        Messages received from a room we're in are passed to receivedGroupChat.
139        """
140        xml = u"""
141            <message to='test@test.com' from='%s' type='groupchat'>
142              <body>test</body>
143            </message>
144        """ % (self.occupantJID)
145
146        self._createRoom()
147
148        def receivedGroupChat(room, user, message):
149            self.assertEquals('test', message.body, "Wrong group chat message")
150            self.assertEquals(self.roomIdentifier, room.roomIdentifier,
151                              'Wrong room name')
152
153        d, self.protocol.receivedGroupChat = calledAsync(receivedGroupChat)
154        self.stub.send(parseXml(xml))
155        return d
156
157
158    def test_receivedGroupChatRoom(self):
159        """
160        Messages received from the room itself have C{user} set to C{None}.
161        """
162        xml = u"""
163            <message to='test@test.com' from='%s' type='groupchat'>
164              <body>test</body>
165            </message>
166        """ % (self.roomJID)
167
168        self._createRoom()
169
170        def receivedGroupChat(room, user, message):
171            self.assertIdentical(None, user)
172
173        d, self.protocol.receivedGroupChat = calledAsync(receivedGroupChat)
174        self.stub.send(parseXml(xml))
175        return d
176
177
178    def test_join(self):
179        """
180        Joining a room waits for confirmation, deferred fires room.
181        """
182
183        def cb(room):
184            self.assertEquals(self.roomIdentifier, room.roomIdentifier)
185
186        d = self.protocol.join(self.service, self.roomIdentifier, self.nick)
187        d.addCallback(cb)
188
189        element = self.stub.output[-1]
190        self.assertEquals('presence', element.name, "Need to be presence")
191        self.assertNotIdentical(None, element.x, 'No muc x element')
192
193        # send back user presence, they joined
194        xml = """
195            <presence from='%s@%s/%s'>
196              <x xmlns='http://jabber.org/protocol/muc#user'>
197                <item affiliation='member' role='participant'/>
198              </x>
199            </presence>
200        """ % (self.roomIdentifier, self.service, self.nick)
201        self.stub.send(parseXml(xml))
202        return d
203
204
205    def test_joinHistory(self):
206        """
207        Passing a history parameter sends a 'maxstanzas' history limit.
208        """
209
210        def cb(room):
211            self.assertEquals(self.roomIdentifier, room.roomIdentifier)
212
213        d = self.protocol.join(self.service, self.roomIdentifier, self.nick,
214                               history=10)
215        d.addCallback(cb)
216
217        element = self.stub.output[-1]
218        query = "/*/x[@xmlns='%s']/history[@xmlns='%s']" % (muc.NS_MUC,
219                                                            muc.NS_MUC)
220        result = xpath.queryForNodes(query, element)
221        history = result[0]
222        self.assertEquals('10', history.getAttribute('maxstanzas'))
223
224        # send back user presence, they joined
225        xml = """
226            <presence from='%s@%s/%s'>
227              <x xmlns='http://jabber.org/protocol/muc#user'>
228                <item affiliation='member' role='participant'/>
229              </x>
230            </presence>
231        """ % (self.roomIdentifier, self.service, self.nick)
232        self.stub.send(parseXml(xml))
233        return d
234
235
236    def test_joinForbidden(self):
237        """
238        A forbidden error in response to a join errbacks with L{StanzaError}.
239        """
240
241        def cb(error):
242            self.assertEquals('forbidden', error.condition,
243                              'Wrong muc condition')
244
245        d = self.protocol.join(self.service, self.roomIdentifier, self.nick)
246        self.assertFailure(d, StanzaError)
247        d.addCallback(cb)
248
249        # send back error, forbidden
250        xml = u"""
251            <presence from='%s' type='error'>
252              <error type='auth'>
253                <forbidden xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
254              </error>
255            </presence>
256        """ % (self.occupantJID)
257        self.stub.send(parseXml(xml))
258        return d
259
260
261    def test_joinForbiddenFromRoomJID(self):
262        """
263        An error response to a join sent from the room JID should errback.
264
265        Some service implementations send error stanzas from the room JID
266        instead of the JID the join presence was sent to.
267        """
268
269        d = self.protocol.join(self.service, self.roomIdentifier, self.nick)
270        self.assertFailure(d, StanzaError)
271
272        # send back error, forbidden
273        xml = u"""
274            <presence from='%s' type='error'>
275              <error type='auth'>
276                <forbidden xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
277              </error>
278            </presence>
279        """ % (self.roomJID)
280        self.stub.send(parseXml(xml))
281        return d
282
283
284    def test_joinBadJID(self):
285        """
286        Client joining a room and getting a jid-malformed error.
287        """
288
289        def cb(error):
290            self.assertEquals('jid-malformed', error.condition,
291                              'Wrong muc condition')
292
293        d = self.protocol.join(self.service, self.roomIdentifier, self.nick)
294        self.assertFailure(d, StanzaError)
295        d.addCallback(cb)
296
297        # send back error, bad JID
298        xml = u"""
299            <presence from='%s' type='error'>
300              <error type='modify'>
301                <jid-malformed xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
302              </error>
303            </presence>
304        """ % (self.occupantJID)
305        self.stub.send(parseXml(xml))
306        return d
307
308
309    def test_joinTimeout(self):
310        """
311        After not receiving a response to a join, errback with L{TimeoutError}.
312        """
313
314        d = self.protocol.join(self.service, self.roomIdentifier, self.nick)
315        self.assertFailure(d, TimeoutError)
316        self.clock.advance(muc.DEFER_TIMEOUT)
317        return d
318
319
320    def test_leave(self):
321        """
322        Client leaves a room
323        """
324        def cb(left):
325            self.assertTrue(left, 'did not leave room')
326
327        self._createRoom()
328        d = self.protocol.leave(self.roomJID)
329        d.addCallback(cb)
330
331        element = self.stub.output[-1]
332
333        self.assertEquals('unavailable', element['type'],
334                          'Unavailable is not being sent')
335
336        xml = u"""
337            <presence to='%s' from='%s' type='unavailable'/>
338        """ % (self.userJID, self.occupantJID)
339        self.stub.send(parseXml(xml))
340        return d
341
342
343    def test_userLeftRoom(self):
344        """
345        Unavailable presence from a participant removes it from the room.
346        """
347
348        xml = u"""
349            <presence to='%s' from='%s' type='unavailable'/>
350        """ % (self.userJID, self.occupantJID)
351
352        # create a room
353        self._createRoom()
354
355        # add user to room
356        user = muc.User(self.nick)
357        room = self.protocol._getRoom(self.roomJID)
358        room.addUser(user)
359
360        def userLeftRoom(room, user):
361            self.assertEquals(self.roomIdentifier, room.roomIdentifier,
362                              'Wrong room name')
363            self.assertFalse(room.inRoster(user), 'User in roster')
364
365        d, self.protocol.userLeftRoom = calledAsync(userLeftRoom)
366        self.stub.send(parseXml(xml))
367        return d
368
369
370    def test_ban(self):
371        """
372        Ban an entity in a room.
373        """
374        banned = JID('ban@jabber.org/TroubleMaker')
375
376        def cb(banned):
377            self.assertTrue(banned, 'Did not ban user')
378
379        d = self.protocol.ban(self.occupantJID, banned, reason='Spam',
380                              sender=self.userJID)
381        d.addCallback(cb)
382
383        iq = self.stub.output[-1]
384
385        self.assertTrue(xpath.matches(
386                u"/iq[@type='set' and @to='%s']/query/item"
387                    "[@affiliation='outcast']" % (self.roomJID,),
388                iq),
389            'Wrong ban stanza')
390
391        response = toResponse(iq, 'result')
392        self.stub.send(response)
393
394        return d
395
396
397    def test_kick(self):
398        """
399        Kick an entity from a room.
400        """
401        nick = 'TroubleMaker'
402
403        def cb(kicked):
404            self.assertTrue(kicked, 'Did not kick user')
405
406        d = self.protocol.kick(self.occupantJID, nick, reason='Spam',
407                               sender=self.userJID)
408        d.addCallback(cb)
409
410        iq = self.stub.output[-1]
411
412        self.assertTrue(xpath.matches(
413                u"/iq[@type='set' and @to='%s']/query/item"
414                    "[@affiliation='none']" % (self.roomJID,),
415                iq),
416            'Wrong kick stanza')
417
418        response = toResponse(iq, 'result')
419        self.stub.send(response)
420
421        return d
422
423
424    def test_password(self):
425        """
426        Sending a password via presence to a password protected room.
427        """
428
429        self.protocol.password(self.occupantJID, 'secret')
430
431        element = self.stub.output[-1]
432
433        self.assertTrue(xpath.matches(
434                u"/presence[@to='%s']/x/password"
435                    "[text()='secret']" % (self.occupantJID,),
436                element),
437            'Wrong presence stanza')
438
439
440    def test_receivedHistory(self):
441        """
442        Receiving history on room join.
443        """
444        xml = u"""
445            <message to='test@test.com' from='%s' type='groupchat'>
446              <body>test</body>
447              <delay xmlns='urn:xmpp:delay' stamp="2002-10-13T23:58:37Z"
448                                            from="%s"/>
449            </message>
450        """ % (self.occupantJID, self.userJID)
451
452        self._createRoom()
453
454
455        def receivedHistory(room, user, message):
456            self.assertEquals('test', message.body, "wrong message body")
457            stamp = datetime(2002, 10, 13, 23, 58, 37, tzinfo=tzutc())
458            self.assertEquals(stamp, message.delay.stamp,
459                             'Does not have a history stamp')
460
461        d, self.protocol.receivedHistory = calledAsync(receivedHistory)
462        self.stub.send(parseXml(xml))
463        return d
464
465
466    def test_oneToOneChat(self):
467        """
468        Converting a one to one chat to a multi-user chat.
469        """
470        archive = []
471        thread = "e0ffe42b28561960c6b12b944a092794b9683a38"
472        # create messages
473        element = domish.Element((None, 'message'))
474        element['to'] = 'testing@example.com'
475        element['type'] = 'chat'
476        element.addElement('body', None, 'test')
477        element.addElement('thread', None, thread)
478
479        archive.append({'stanza': element,
480                        'timestamp': datetime(2002, 10, 13, 23, 58, 37,
481                                              tzinfo=tzutc())})
482
483        element = domish.Element((None, 'message'))
484        element['to'] = 'testing2@example.com'
485        element['type'] = 'chat'
486        element.addElement('body', None, 'yo')
487        element.addElement('thread', None, thread)
488
489        archive.append({'stanza': element,
490                        'timestamp': datetime(2002, 10, 13, 23, 58, 43,
491                                              tzinfo=tzutc())})
492
493        self.protocol.history(self.occupantJID, archive)
494
495
496        while len(self.stub.output)>0:
497            element = self.stub.output.pop()
498            # check for delay element
499            self.assertEquals('message', element.name, 'Wrong stanza')
500            self.assertTrue(xpath.matches("/message/delay", element),
501                            'Invalid history stanza')
502
503
504    def test_invite(self):
505        """
506        Invite a user to a room
507        """
508        invitee = JID('other@example.org')
509
510        self.protocol.invite(self.roomJID, invitee, u'This is a test')
511
512        message = self.stub.output[-1]
513
514        self.assertEquals('message', message.name)
515        self.assertEquals(self.roomJID.full(), message.getAttribute('to'))
516        self.assertEquals(muc.NS_MUC_USER, message.x.uri)
517        self.assertEquals(muc.NS_MUC_USER, message.x.invite.uri)
518        self.assertEquals(invitee.full(), message.x.invite.getAttribute('to'))
519        self.assertEquals(muc.NS_MUC_USER, message.x.invite.reason.uri)
520        self.assertEquals(u'This is a test', unicode(message.x.invite.reason))
521
522
523    def test_groupChat(self):
524        """
525        Send private messages to muc entities.
526        """
527        self.protocol.groupChat(self.roomJID, u'This is a test')
528
529        message = self.stub.output[-1]
530
531        self.assertEquals('message', message.name)
532        self.assertEquals(self.roomJID.full(), message.getAttribute('to'))
533        self.assertEquals('groupchat', message.getAttribute('type'))
534        self.assertEquals(u'This is a test', unicode(message.body))
535
536
537    def test_chat(self):
538        """
539        Send private messages to muc entities.
540        """
541        otherOccupantJID = JID(self.occupantJID.userhost()+'/OtherNick')
542
543        self.protocol.chat(otherOccupantJID, u'This is a test')
544
545        message = self.stub.output[-1]
546
547        self.assertEquals('message', message.name)
548        self.assertEquals(otherOccupantJID.full(), message.getAttribute('to'))
549        self.assertEquals('chat', message.getAttribute('type'))
550        self.assertEquals(u'This is a test', unicode(message.body))
551
552
553    def test_register(self):
554        """
555        Client registering with a room.
556
557        http://xmpp.org/extensions/xep-0045.html#register
558        """
559
560        # FIXME: this doesn't really test the registration
561
562        def cb(iq):
563            # check for a result
564            self.assertEquals('result', iq['type'], 'We did not get a result')
565
566        d = self.protocol.register(self.roomJID)
567        d.addCallback(cb)
568
569        iq = self.stub.output[-1]
570        query = "/iq/query[@xmlns='%s']" % muc.NS_REQUEST
571        self.assertTrue(xpath.matches(query, iq), 'Invalid iq register request')
572
573        response = toResponse(iq, 'result')
574        self.stub.send(response)
575        return d
576
577
578    def test_voice(self):
579        """
580        Client requesting voice for a room.
581        """
582        self.protocol.voice(self.occupantJID)
583
584        m = self.stub.output[-1]
585
586        query = ("/message/x[@type='submit']/field/value"
587                    "[text()='%s']") % muc.NS_MUC_REQUEST
588        self.assertTrue(xpath.matches(query, m), 'Invalid voice message stanza')
589
590
591    def test_roomConfigure(self):
592        """
593        Default configure and changing the room name.
594        """
595
596        def cb(iq):
597            self.assertEquals('result', iq['type'], 'Not a result')
598
599
600        fields = []
601
602        fields.append(data_form.Field(label='Natural-Language Room Name',
603                                      var='muc#roomconfig_roomname',
604                                      value=self.roomIdentifier))
605
606        d = self.protocol.configure(self.roomJID, fields)
607        d.addCallback(cb)
608
609        iq = self.stub.output[-1]
610        query = "/iq/query[@xmlns='%s']/x"% muc.NS_MUC_OWNER
611        self.assertTrue(xpath.matches(query, iq), 'Bad configure request')
612
613        response = toResponse(iq, 'result')
614        self.stub.send(response)
615        return d
616
617
618    def test_destroy(self):
619        """
620        Destroy a room.
621        """
622
623        def cb(destroyed):
624            self.assertTrue(destroyed, 'Room not destroyed.')
625
626        d = self.protocol.destroy(self.occupantJID)
627        d.addCallback(cb)
628
629        iq = self.stub.output[-1]
630        query = "/iq/query[@xmlns='%s']/destroy"% muc.NS_MUC_OWNER
631        self.assertTrue(xpath.matches(query, iq), 'Bad configure request')
632
633        response = toResponse(iq, 'result')
634        self.stub.send(response)
635        return d
636
637
638    def test_subject(self):
639        """
640        Change subject of the room.
641        """
642        self.protocol.subject(self.roomJID, u'This is a test')
643
644        message = self.stub.output[-1]
645
646        self.assertEquals('message', message.name)
647        self.assertEquals(self.roomJID.full(), message.getAttribute('to'))
648        self.assertEquals('groupchat', message.getAttribute('type'))
649        self.assertEquals(u'This is a test', unicode(message.subject))
650
651
652    def test_nick(self):
653        """
654        Send a nick change to the server.
655        """
656        newNick = 'newNick'
657
658        self._createRoom()
659
660        def cb(room):
661            self.assertEquals(self.roomIdentifier, room.roomIdentifier)
662            self.assertEquals(newNick, room.nick)
663
664        d = self.protocol.nick(self.roomJID, newNick)
665        d.addCallback(cb)
666
667        element = self.stub.output[-1]
668        self.assertEquals('presence', element.name, "Need to be presence")
669        self.assertNotIdentical(None, element.x, 'No muc x element')
670
671        # send back user presence, nick changed
672        xml = u"""
673            <presence from='%s/%s'>
674              <x xmlns='http://jabber.org/protocol/muc#user'>
675                <item affiliation='member' role='participant'/>
676              </x>
677            </presence>
678        """ % (self.roomJID, newNick)
679        self.stub.send(parseXml(xml))
680        return d
681
682
683    def test_nickConflict(self):
684        """
685        If the server finds the new nick in conflict, the errback is called.
686        """
687        newNick = 'newNick'
688
689        self._createRoom()
690
691        d = self.protocol.nick(self.roomJID, newNick)
692        self.assertFailure(d, StanzaError)
693
694        element = self.stub.output[-1]
695        self.assertEquals('presence', element.name, "Need to be presence")
696        self.assertNotIdentical(None, element.x, 'No muc x element')
697
698        # send back user presence, nick changed
699        xml = u"""
700            <presence from='%s/%s' type='error'>
701                <x xmlns='http://jabber.org/protocol/muc'/>
702                <error type='cancel'>
703                  <conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
704                </error>
705            </presence>
706        """ % (self.roomJID, newNick)
707        self.stub.send(parseXml(xml))
708        return d
709
710
711    def test_grantVoice(self):
712        """
713        Test granting voice to a user.
714
715        """
716        nick = 'TroubleMaker'
717        def cb(give_voice):
718            self.assertTrue(give_voice, 'Did not give voice user')
719
720        d = self.protocol.grantVoice(self.occupantJID, nick,
721                                     sender=self.userJID)
722        d.addCallback(cb)
723
724        iq = self.stub.output[-1]
725
726        query = (u"/iq[@type='set' and @to='%s']/query/item"
727                     "[@role='participant']") % self.roomJID
728        self.assertTrue(xpath.matches(query, iq), 'Wrong voice stanza')
729
730        response = toResponse(iq, 'result')
731        self.stub.send(response)
732        return d
733
734
735    def test_status(self):
736        """
737        Change status
738        """
739        self._createRoom()
740        room = self.protocol._getRoom(self.roomJID)
741        user = muc.User(self.nick)
742        room.addUser(user)
743
744        def cb(room):
745            self.assertEquals(self.roomIdentifier, room.roomIdentifier)
746            user = room.getUser(self.nick)
747            self.assertNotIdentical(None, user, 'User not found')
748            self.assertEquals('testing MUC', user.status, 'Wrong status')
749            self.assertEquals('xa', user.show, 'Wrong show')
750
751        d = self.protocol.status(self.roomJID, 'xa', 'testing MUC')
752        d.addCallback(cb)
753
754        element = self.stub.output[-1]
755
756        self.assertEquals('presence', element.name, "Need to be presence")
757        self.assertTrue(getattr(element, 'x', None), 'No muc x element')
758
759        # send back user presence, status changed
760        xml = u"""
761            <presence from='%s'>
762              <x xmlns='http://jabber.org/protocol/muc#user'>
763                <item affiliation='member' role='participant'/>
764              </x>
765              <show>xa</show>
766              <status>testing MUC</status>
767            </presence>
768        """ % self.occupantJID
769        self.stub.send(parseXml(xml))
770        return d
771
772
773    def test_getMemberList(self):
774        def cb(room):
775            members = room.members
776            self.assertEquals(1, len(members))
777            user = members[0]
778            self.assertEquals(JID(u'hag66@shakespeare.lit'), user.entity)
779            self.assertEquals(u'thirdwitch', user.nick)
780            self.assertEquals(u'participant', user.role)
781
782        self._createRoom()
783        d = self.protocol.getMemberList(self.roomJID)
784        d.addCallback(cb)
785
786        iq = self.stub.output[-1]
787        query = iq.query
788        self.assertNotIdentical(None, query)
789        self.assertEquals(NS_MUC_ADMIN, query.uri)
790
791        response = toResponse(iq, 'result')
792        query = response.addElement((NS_MUC_ADMIN, 'query'))
793        item = query.addElement('item')
794        item['affiliation'] ='member'
795        item['jid'] = 'hag66@shakespeare.lit'
796        item['nick'] = 'thirdwitch'
797        item['role'] = 'participant'
798        self.stub.send(response)
799
800        return d
Note: See TracBrowser for help on using the repository browser.