source: ralphm-patches/roster_item.patch @ 49:537d1413b661

Last change on this file since 49:537d1413b661 was 49:537d1413b661, checked in by Ralph Meijer <ralphm@…>, 9 years ago

Save work after moving stuff to keep from wokkel.xmppim to wokkel.im.

File size: 24.4 KB
  • wokkel/im.py

    diff -r c91f18811c37 wokkel/im.py
    a b  
    77XMPP IM protocol support.
    88
    99This module provides generic implementations for the protocols defined in
    10 U{RFC 3921<http://www.xmpp.org/rfcs/rfc3921.html>} (XMPP IM).
    11 
    12 All of it should eventually move to Twisted.
     10U{RFC 6121<http://www.xmpp.org/rfcs/rfc6121.html>} (XMPP IM).
    1311"""
    1412
     13import warnings
     14
     15from twisted.internet import defer
    1516from twisted.words.protocols.jabber.jid import JID, internJID
     17from twisted.words.protocols.jabber import error
    1618from twisted.words.xish import domish
    1719
    1820from wokkel.compat import IQ
    1921from wokkel.generic import ErrorStanza, Stanza
    20 from wokkel.subprotocols import XMPPHandler
     22from wokkel.subprotocols import IQHandlerMixin, XMPPHandler
    2123
    2224NS_XML = 'http://www.w3.org/XML/1998/namespace'
    2325NS_ROSTER = 'jabber:iq:roster'
    2426
     27XPATH_ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
     28
     29
     30
    2531class BasePresence(Stanza):
    2632    """
    2733    Stanza of kind presence.
     
    349355
    350356    This represents one contact from an XMPP contact list known as roster.
    351357
    352     @ivar jid: The JID of the contact.
    353     @type jid: L{JID}
     358    @ivar entity: The JID of the contact.
     359    @type entity: L{JID}
    354360    @ivar name: The optional associated nickname for this contact.
    355361    @type name: C{unicode}
    356362    @ivar subscriptionTo: Subscription state to contact's presence. If C{True},
     
    360366    @ivar subscriptionFrom: Contact's subscription state. If C{True}, the
    361367                            contact is subscribed to the presence information
    362368                            of the roster owner.
    363     @type subscriptionTo: C{bool}
    364     @ivar ask: Whether subscription is pending.
    365     @type ask: C{bool}
     369    @type subscriptionFrom: C{bool}
     370    @ivar pendingOut: Whether the subscription request to this contact is
     371        pending.
     372    @type pendingOut: C{bool}
    366373    @ivar groups: Set of groups this contact is categorized in. Groups are
    367374                  represented by an opaque identifier of type C{unicode}.
    368375    @type groups: C{set}
    369376    """
    370377
    371     def __init__(self, jid):
    372         self.jid = jid
    373         self.name = None
    374         self.subscriptionTo = False
    375         self.subscriptionFrom = False
    376         self.ask = None
    377         self.groups = set()
     378    __subscriptionStates = {(False, False): None,
     379                            (True, False): 'to',
     380                            (False, True): 'from',
     381                            (True, True): 'both'}
    378382
     383    def __init__(self, entity, subscriptionTo=False, subscriptionFrom=False,
     384                       name=None, groups=None):
     385        self.entity = entity
     386        self.subscriptionTo = subscriptionTo
     387        self.subscriptionFrom = subscriptionFrom
     388        self.name = name
     389        self.groups = groups or set()
    379390
     391        self.pendingOut = False
     392        self.approved = False
     393        self.remove = False
    380394
    381 class RosterClientProtocol(XMPPHandler):
     395
     396    def __getJID(self):
     397        warnings.warn("Use RosterItem.entity instead.", DeprecationWarning)
     398        return self.entity
     399
     400
     401    def __setJID(self, value):
     402        warnings.warn("Use RosterItem.entity instead.", DeprecationWarning)
     403        self.entity = value
     404
     405
     406    jid = property(__getJID, __setJID, doc="""
     407            JID of the contact. Deprecated in favour of C{entity}.""")
     408
     409
     410    def __getAsk(self):
     411        warnings.warn("Use RosterItem.pendingOut instead.", DeprecationWarning)
     412        return self.pendingOut
     413
     414
     415    def __setAsk(self, value):
     416        warnings.warn("Use RosterItem.pendingOut instead.", DeprecationWarning)
     417        self.pendingOut = value
     418
     419
     420    ask = property(__getAsk, __setAsk, doc="""
     421            Pending out subscription. Deprecated in favour of C{pendingOut}.""")
     422
     423
     424    def toElement(self):
     425        element = domish.Element((NS_ROSTER, 'item'))
     426        element['jid'] = self.entity.full()
     427
     428        if self.remove:
     429            subscription = 'remove'
     430        else:
     431            subscription = self.__subscriptionStates[self.subscriptionTo,
     432                                                     self.subscriptionFrom]
     433
     434            if self.pendingOut:
     435                element['ask'] = u'subscribe'
     436
     437            if self.name:
     438                element['name'] = self.name
     439
     440            if self.approved:
     441                element['approved'] = u'true'
     442
     443            if self.groups:
     444                for group in self.groups:
     445                    element.addElement('group', content=group)
     446
     447        if subscription:
     448            element['subscription'] = subscription
     449
     450        return element
     451
     452
     453    @classmethod
     454    def fromElement(Class, element):
     455        entity = internJID(element['jid'])
     456        item = Class(entity)
     457        subscription = element.getAttribute('subscription')
     458        if subscription == 'remove':
     459            item.remove = True
     460        else:
     461            item.name = element.getAttribute('name')
     462            item.subscriptionTo = subscription in ('to', 'both')
     463            item.subscriptionFrom = subscription in ('from', 'both')
     464            item.pendingOut = element.getAttribute('ask') == 'subscribe'
     465            item.approved = element.getAttribute('approved') in ('true', '1')
     466            for subElement in domish.generateElementsQNamed(element.children,
     467                                                            'group', NS_ROSTER):
     468                item.groups.add(unicode(subElement))
     469        return item
     470
     471
     472
     473class RosterPushIgnored(Exception):
     474    """
     475    Raised when this entity doesn't want to accept/trust a roster push.
     476    """
     477
     478
     479
     480class RosterClientProtocol(XMPPHandler, IQHandlerMixin):
    382481    """
    383482    Client side XMPP roster protocol.
    384483    """
    385484
     485    iqHandlers = {XPATH_ROSTER_SET: "_onRosterSet"}
     486
    386487    def connectionInitialized(self):
    387         ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
    388         self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet)
    389 
    390 
    391     def _parseRosterItem(self, element):
    392         jid = internJID(element['jid'])
    393         item = RosterItem(jid)
    394         item.name = element.getAttribute('name')
    395         subscription = element.getAttribute('subscription')
    396         item.subscriptionTo = subscription in ('to', 'both')
    397         item.subscriptionFrom = subscription in ('from', 'both')
    398         item.ask = element.getAttribute('ask') == 'subscribe'
    399         for subElement in domish.generateElementsQNamed(element.children,
    400                                                         'group', NS_ROSTER):
    401             item.groups.add(unicode(subElement))
    402 
    403         return item
     488        self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest)
    404489
    405490
    406491    def getRoster(self):
     
    415500            roster = {}
    416501            for element in domish.generateElementsQNamed(result.query.children,
    417502                                                         'item', NS_ROSTER):
    418                 item = self._parseRosterItem(element)
    419                 roster[item.jid.userhost()] = item
     503                item = RosterItem.fromElement(element)
     504                roster[item.entity] = item
    420505
    421506            return roster
    422507
     
    437522        """
    438523        iq = IQ(self.xmlstream, 'set')
    439524        iq.addElement((NS_ROSTER, 'query'))
    440         item = iq.query.addElement('item')
    441         item['jid'] = entity.full()
    442         item['subscription'] = 'remove'
     525        item = RosterItem(entity)
     526        item.remove = True
     527        iq.query.addChild(item.toElement())
    443528        return iq.send()
    444529
    445530
    446531    def _onRosterSet(self, iq):
    447         if iq.handled or \
    448            iq.hasAttribute('from') and iq['from'] != self.xmlstream:
    449             return
    450 
    451         iq.handled = True
     532        def eb(failure):
     533            failure.trap(RosterPushIgnored)
     534            raise error.StanzaError('service-unavailable')
    452535
    453536        itemElement = iq.query.item
     537        item = RosterItem.fromElement(iq.query.item)
    454538
    455         if unicode(itemElement['subscription']) == 'remove':
    456             self.onRosterRemove(internJID(itemElement['jid']))
     539        if item.remove:
     540            d = defer.maybeDeferred(self.onRosterRemove, item.entity)
    457541        else:
    458             item = self._parseRosterItem(iq.query.item)
    459             self.onRosterSet(item)
     542            d = defer.maybeDeferred(self.onRosterSet, item)
     543
     544        d.addErrback(eb)
     545        return d
    460546
    461547
    462548    def onRosterSet(self, item):
    463549        """
    464550        Called when a roster push for a new or update item was received.
    465551
     552        Raise L{RosterPushIgnored} when not accepting this roster push
     553        (directly or via Deferred). This will result in a
     554        L{'service-unavailable'} error being sent in return.
     555
    466556        @param item: The pushed roster item.
    467557        @type item: L{RosterItem}
    468558        """
     
    472562        """
    473563        Called when a roster push for the removal of an item was received.
    474564
     565        Raise L{RosterPushIgnored} when not accepting this roster push
     566        (directly or via Deferred). This will result in a
     567        L{'service-unavailable'} error being sent in return.
     568
    475569        @param entity: The entity for which the roster item has been removed.
    476570        @type entity: L{JID}
    477571        """
  • wokkel/test/test_im.py

    diff -r c91f18811c37 wokkel/test/test_im.py
    a b  
    77
    88from twisted.internet import defer
    99from twisted.trial import unittest
     10from twisted.words.protocols.jabber import error
    1011from twisted.words.protocols.jabber.jid import JID
    1112from twisted.words.protocols.jabber.xmlstream import toResponse
    1213from twisted.words.xish import domish, utility
    1314
    1415from wokkel import im
    1516from wokkel.generic import ErrorStanza, parseXml
    16 from wokkel.test.helpers import XmlStreamStub
     17from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
    1718
    1819NS_XML = 'http://www.w3.org/XML/1998/namespace'
    1920NS_ROSTER = 'jabber:iq:roster'
     
    389390
    390391
    391392
    392 class RosterClientProtocolTest(unittest.TestCase):
     393class RosterItemTest(unittest.TestCase):
     394    """
     395    Tests for L{im.RosterItem}.
     396    """
     397
     398    def test_jidDeprecationGet(self):
     399        """
     400        Getting the jid attribute works as entity and warns deprecation.
     401        """
     402        item = im.RosterItem(JID('user@example.org'))
     403        entity = self.assertWarns(DeprecationWarning,
     404                                  "Use RosterItem.entity instead.",
     405                                  im.__file__,
     406                                  getattr, item, 'jid')
     407        self.assertIdentical(entity, item.entity)
     408
     409
     410    def test_jidDeprecationSet(self):
     411        """
     412        Setting the jid attribute works as entity and warns deprecation.
     413        """
     414        item = im.RosterItem(JID('user@example.org'))
     415        entity = self.assertWarns(DeprecationWarning,
     416                                  "Use RosterItem.entity instead.",
     417                                  im.__file__,
     418                                  setattr, item, 'jid',
     419                                  JID('other@example.org'))
     420        self.assertEquals(JID('other@example.org'), item.entity)
     421
     422
     423    def test_askDeprecationGet(self):
     424        """
     425        Getting the ask attribute works as entity and warns deprecation.
     426        """
     427        item = im.RosterItem(JID('user@example.org'))
     428        item.pendingOut = True
     429        ask = self.assertWarns(DeprecationWarning,
     430                               "Use RosterItem.pendingOut instead.",
     431                               im.__file__,
     432                               getattr, item, 'ask')
     433        self.assertTrue(ask)
     434
     435
     436    def test_askDeprecationSet(self):
     437        """
     438        Setting the ask attribute works as entity and warns deprecation.
     439        """
     440        item = im.RosterItem(JID('user@example.org'))
     441        entity = self.assertWarns(DeprecationWarning,
     442                                  "Use RosterItem.pendingOut instead.",
     443                                  im.__file__,
     444                                  setattr, item, 'ask',
     445                                  True)
     446        self.assertTrue(item.pendingOut)
     447
     448
     449    def test_toElement(self):
     450        item = im.RosterItem(JID('user@example.org'))
     451        element = item.toElement()
     452        self.assertEquals('item', element.name)
     453        self.assertEquals(NS_ROSTER, element.uri)
     454        self.assertFalse(element.hasAttribute('subscription'))
     455        self.assertFalse(element.hasAttribute('ask'))
     456        self.assertFalse(element.hasAttribute('name'))
     457        self.assertFalse(element.hasAttribute('approved'))
     458        self.assertEquals(0, len(list(element.elements())))
     459
     460
     461    def test_toElementMinimal(self):
     462        item = im.RosterItem(JID('user@example.org'))
     463        element = item.toElement()
     464        self.assertEquals(u'user@example.org', element.getAttribute('jid'))
     465
     466
     467    def test_toElementSubscriptionNone(self):
     468        item = im.RosterItem(JID('user@example.org'),
     469                                 subscriptionTo=False,
     470                                 subscriptionFrom=False)
     471        element = item.toElement()
     472        self.assertIdentical(None, element.getAttribute('subscription'))
     473
     474
     475    def test_toElementSubscriptionTo(self):
     476        item = im.RosterItem(JID('user@example.org'),
     477                                 subscriptionTo=True,
     478                                 subscriptionFrom=False)
     479        element = item.toElement()
     480        self.assertEquals('to', element.getAttribute('subscription'))
     481
     482
     483    def test_toElementSubscriptionFrom(self):
     484        item = im.RosterItem(JID('user@example.org'),
     485                                 subscriptionTo=False,
     486                                 subscriptionFrom=True)
     487        element = item.toElement()
     488        self.assertEquals('from', element.getAttribute('subscription'))
     489
     490
     491    def test_toElementSubscriptionBoth(self):
     492        item = im.RosterItem(JID('user@example.org'),
     493                                 subscriptionTo=True,
     494                                 subscriptionFrom=True)
     495        element = item.toElement()
     496        self.assertEquals('both', element.getAttribute('subscription'))
     497
     498
     499    def test_toElementSubscriptionRemove(self):
     500        item = im.RosterItem(JID('user@example.org'))
     501        item.remove = True
     502        element = item.toElement()
     503        self.assertEquals('remove', element.getAttribute('subscription'))
     504
     505
     506    def test_toElementAsk(self):
     507        item = im.RosterItem(JID('user@example.org'))
     508        item.pendingOut = True
     509        element = item.toElement()
     510        self.assertEquals('subscribe', element.getAttribute('ask'))
     511
     512
     513    def test_toElementName(self):
     514        item = im.RosterItem(JID('user@example.org'),
     515                                 name='Joe User')
     516        element = item.toElement()
     517        self.assertEquals(u'Joe User', element.getAttribute('name'))
     518
     519
     520    def test_toElementGroups(self):
     521        groups = set(['Friends', 'Jabber'])
     522        item = im.RosterItem(JID('user@example.org'),
     523                                 groups=groups)
     524
     525        element = item.toElement()
     526        foundGroups = set()
     527        for child in element.elements():
     528            if child.uri == NS_ROSTER and child.name == 'group':
     529                foundGroups.add(unicode(child))
     530
     531        self.assertEqual(groups, foundGroups)
     532
     533
     534    def test_toElementApproved(self):
     535        """
     536        A pre-approved subscription for a roster item has an 'approved' flag.
     537        """
     538        item = im.RosterItem(JID('user@example.org'))
     539        item.approved = True
     540        element = item.toElement()
     541        self.assertEquals(u'true', element.getAttribute('approved'))
     542
     543
     544    def test_fromElementMinimal(self):
     545        """
     546        A minimal roster item has a reference to the JID of the contact.
     547        """
     548
     549        xml = """
     550            <item xmlns="jabber:iq:roster"
     551                  jid="test@example.org"/>
     552        """
     553
     554        item = im.RosterItem.fromElement(parseXml(xml))
     555        self.assertEqual(JID(u"test@example.org"), item.entity)
     556        self.assertIdentical(None, item.name)
     557        self.assertFalse(item.subscriptionTo)
     558        self.assertFalse(item.subscriptionFrom)
     559        self.assertFalse(item.pendingOut)
     560        self.assertFalse(item.approved)
     561        self.assertEquals(set(), item.groups)
     562
     563
     564    def test_fromElementName(self):
     565        """
     566        A roster item may have an optional name.
     567        """
     568
     569        xml = """
     570            <item xmlns="jabber:iq:roster"
     571                  jid="test@example.org"
     572                  name="Test User"/>
     573        """
     574
     575        item = im.RosterItem.fromElement(parseXml(xml))
     576        self.assertEqual(u"Test User", item.name)
     577
     578
     579    def test_fromElementGroups(self):
     580        """
     581        A roster item may have one or more groups.
     582        """
     583
     584        xml = """
     585            <item xmlns="jabber:iq:roster"
     586                  jid="test@example.org">
     587              <group>Friends</group>
     588              <group>Twisted</group>
     589            </item>
     590        """
     591
     592        item = im.RosterItem.fromElement(parseXml(xml))
     593        self.assertIn(u"Twisted", item.groups)
     594        self.assertIn(u"Friends", item.groups)
     595
     596
     597    def test_fromElementSubscriptionNone(self):
     598        """
     599        Subscription 'none' sets both attributes to False.
     600        """
     601
     602        xml = """
     603            <item xmlns="jabber:iq:roster"
     604                  jid="test@example.org"
     605                  subscription="none"/>
     606        """
     607
     608        item = im.RosterItem.fromElement(parseXml(xml))
     609        self.assertFalse(item.remove)
     610        self.assertFalse(item.subscriptionTo)
     611        self.assertFalse(item.subscriptionFrom)
     612
     613
     614    def test_fromElementSubscriptionTo(self):
     615        """
     616        Subscription 'to' sets the corresponding attribute to True.
     617        """
     618
     619        xml = """
     620            <item xmlns="jabber:iq:roster"
     621                  jid="test@example.org"
     622                  subscription="to"/>
     623        """
     624
     625        item = im.RosterItem.fromElement(parseXml(xml))
     626        self.assertFalse(item.remove)
     627        self.assertTrue(item.subscriptionTo)
     628        self.assertFalse(item.subscriptionFrom)
     629
     630
     631    def test_fromElementSubscriptionFrom(self):
     632        """
     633        Subscription 'from' sets the corresponding attribute to True.
     634        """
     635
     636        xml = """
     637            <item xmlns="jabber:iq:roster"
     638                  jid="test@example.org"
     639                  subscription="from"/>
     640        """
     641
     642        item = im.RosterItem.fromElement(parseXml(xml))
     643        self.assertFalse(item.remove)
     644        self.assertFalse(item.subscriptionTo)
     645        self.assertTrue(item.subscriptionFrom)
     646
     647
     648    def test_fromElementSubscriptionBoth(self):
     649        """
     650        Subscription 'both' sets both attributes to True.
     651        """
     652
     653        xml = """
     654            <item xmlns="jabber:iq:roster"
     655                  jid="test@example.org"
     656                  subscription="both"/>
     657        """
     658
     659        item = im.RosterItem.fromElement(parseXml(xml))
     660        self.assertFalse(item.remove)
     661        self.assertTrue(item.subscriptionTo)
     662        self.assertTrue(item.subscriptionFrom)
     663
     664
     665    def test_fromElementSubscriptionRemove(self):
     666        """
     667        Subscription 'remove' sets the remove attribute.
     668        """
     669
     670        xml = """
     671            <item xmlns="jabber:iq:roster"
     672                  jid="test@example.org"
     673                  subscription="remove"/>
     674        """
     675
     676        item = im.RosterItem.fromElement(parseXml(xml))
     677        self.assertTrue(item.remove)
     678
     679
     680    def test_fromElementPendingOut(self):
     681        """
     682        The ask attribute, if set to 'subscription', means pending out.
     683        """
     684
     685        xml = """
     686            <item xmlns="jabber:iq:roster"
     687                  jid="test@example.org"
     688                  ask="subscribe"/>
     689        """
     690
     691        item = im.RosterItem.fromElement(parseXml(xml))
     692        self.assertTrue(item.pendingOut)
     693
     694
     695    def test_fromElementApprovedTrue(self):
     696        """
     697        The approved attribute (true) signals a pre-approved subscription.
     698        """
     699
     700        xml = """
     701            <item xmlns="jabber:iq:roster"
     702                  jid="test@example.org"
     703                  approved="true"/>
     704        """
     705
     706        item = im.RosterItem.fromElement(parseXml(xml))
     707        self.assertTrue(item.approved)
     708
     709
     710    def test_fromElementApproved1(self):
     711        """
     712        The approved attribute (1) signals a pre-approved subscription.
     713        """
     714
     715        xml = """
     716            <item xmlns="jabber:iq:roster"
     717                  jid="test@example.org"
     718                  approved="1"/>
     719        """
     720
     721        item = im.RosterItem.fromElement(parseXml(xml))
     722        self.assertTrue(item.approved)
     723
     724
     725
     726class RosterClientProtocolTest(unittest.TestCase, TestableRequestHandlerMixin):
    393727    """
    394728    Tests for L{im.RosterClientProtocol}.
    395729    """
    396730
    397731    def setUp(self):
    398732        self.stub = XmlStreamStub()
    399         self.protocol = im.RosterClientProtocol()
    400         self.protocol.xmlstream = self.stub.xmlstream
    401         self.protocol.connectionInitialized()
     733        self.service = im.RosterClientProtocol()
     734        self.service.makeConnection(self.stub.xmlstream)
     735        self.service.connectionInitialized()
    402736
    403737
    404738    def test_removeItem(self):
    405739        """
    406740        Removing a roster item is setting an item with subscription C{remove}.
    407741        """
    408         d = self.protocol.removeItem(JID('test@example.org'))
     742        d = self.service.removeItem(JID('test@example.org'))
    409743
    410744        # Inspect outgoing iq request
    411745
     
    419753        self.assertEquals(1, len(children))
    420754        child = children[0]
    421755        self.assertEquals('test@example.org', child['jid'])
    422         self.assertEquals('remove', child['subscription'])
     756        self.assertEquals('remove', child.getAttribute('subscription'))
    423757
    424758        # Fake successful response
    425759
    426760        response = toResponse(iq, 'result')
    427761        self.stub.send(response)
    428762        return d
     763
     764
     765    def test_getRoster(self):
     766        def cb(roster):
     767            self.assertIn(JID('user@example.org'), roster)
     768
     769
     770        d = self.service.getRoster()
     771        d.addCallback(cb)
     772
     773        # Inspect outgoing iq request
     774
     775        iq = self.stub.output[-1]
     776        self.assertEquals('get', iq.getAttribute('type'))
     777        self.assertNotIdentical(None, iq.query)
     778        self.assertEquals(NS_ROSTER, iq.query.uri)
     779
     780        # Fake successful response
     781        response = toResponse(iq, 'result')
     782        query = response.addElement((NS_ROSTER, 'query'))
     783        item = query.addElement('item')
     784        item['jid'] = 'user@example.org'
     785
     786        self.stub.send(response)
     787        return d
     788
     789
     790    def test_onRosterSet(self):
     791        """
     792        A roster push causes onRosterSet to be called with the parsed item.
     793        """
     794        xml = """
     795          <iq type='set'>
     796            <query xmlns='jabber:iq:roster'>
     797              <item jid='user@example.org'/>
     798            </query>
     799          </iq>
     800        """
     801
     802        items = []
     803
     804        def onRosterSet(item):
     805            items.append(item)
     806
     807        def cb(result):
     808            self.assertEquals(1, len(items))
     809            self.assertEquals(JID('user@example.org'), items[0].entity)
     810
     811        self.service.onRosterSet = onRosterSet
     812
     813        d = self.handleRequest(xml)
     814        d.addCallback(cb)
     815        return d
     816
     817
     818    def test_onRosterSetUntrusted(self):
     819        """
     820        Roster pushes from untrusted sources will be not be handled.
     821        """
     822        xml = """
     823          <iq type='set' from='bad@example.org'>
     824            <query xmlns='jabber:iq:roster'>
     825              <item jid='user@example.org'/>
     826            </query>
     827          </iq>
     828        """
     829
     830        def onRosterSet(item):
     831            raise im.RosterPushIgnored()
     832
     833        def cb(result):
     834            self.assertEquals('service-unavailable', result.condition)
     835
     836        self.service.onRosterSet = onRosterSet
     837
     838        d = self.handleRequest(xml)
     839        self.assertFailure(d, error.StanzaError)
     840        d.addCallback(cb)
     841        return d
     842
     843
     844    def test_onRosterRemove(self):
     845        """
     846        A roster push causes onRosterSet to be called with the parsed item.
     847        """
     848        xml = """
     849          <iq type='set'>
     850            <query xmlns='jabber:iq:roster'>
     851              <item jid='user@example.org' subscription='remove'/>
     852            </query>
     853          </iq>
     854        """
     855
     856        entities = []
     857
     858        def onRosterRemove(entity):
     859            entities.append(entity)
     860
     861        def cb(result):
     862            self.assertEquals([JID('user@example.org')], entities)
     863
     864        self.service.onRosterRemove = onRosterRemove
     865
     866        d = self.handleRequest(xml)
     867        d.addCallback(cb)
     868        return d
     869
Note: See TracBrowser for help on using the repository browser.