Changeset 58:4b02ff624ab2 in ralphm-patches for roster_item.patch


Ignore:
Timestamp:
Apr 29, 2012, 7:10:55 PM (9 years ago)
Author:
Ralph Meijer <ralphm@…>
Branch:
default
Message:

Move roster patches to xmppim.py, add backwards compat and new pushReceived.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • roster_item.patch

    r57 r58  
    88
    99`RosterClientProtocol`:
    10 
    1110 * Roster returned from `getRoster` is now indexed by `JID`s (instead of
    1211   the `unicode` representation of the JID.
    13  * `onRosterSet` and `onRosterRemove` can raise `RosterPushIgnored` to
    14    return a `service-unavailable` stanza error.
     12 * `onRosterSet` and `onRosterRemove` are deprecated in favor of
     13   `pushReceived`, which is called with a `RosterItem` for all roster pushes.
    1514
    16 diff -r a3c50205821b wokkel/im.py
    17 --- a/wokkel/im.py      Wed Mar 28 13:13:06 2012 +0200
    18 +++ b/wokkel/im.py      Wed Mar 28 13:17:05 2012 +0200
    19 @@ -7,21 +7,26 @@
     15diff --git a/wokkel/test/test_xmppim.py b/wokkel/test/test_xmppim.py
     16--- a/wokkel/test/test_xmppim.py
     17+++ b/wokkel/test/test_xmppim.py
     18@@ -7,13 +7,14 @@
     19 
     20 from twisted.internet import defer
     21 from twisted.trial import unittest
     22+from twisted.words.protocols.jabber import error
     23 from twisted.words.protocols.jabber.jid import JID
     24 from twisted.words.protocols.jabber.xmlstream import toResponse
     25 from twisted.words.xish import domish, utility
     26 
     27 from wokkel import xmppim
     28 from wokkel.generic import ErrorStanza, parseXml
     29-from wokkel.test.helpers import XmlStreamStub
     30+from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
     31 
     32 NS_XML = 'http://www.w3.org/XML/1998/namespace'
     33 NS_ROSTER = 'jabber:iq:roster'
     34@@ -449,23 +450,360 @@
     35 
     36 
     37 
     38-class RosterClientProtocolTest(unittest.TestCase):
     39+class RosterItemTest(unittest.TestCase):
     40+    """
     41+    Tests for L{xmppim.RosterItem}.
     42+    """
     43+
     44+    def test_toElement(self):
     45+        item = xmppim.RosterItem(JID('user@example.org'))
     46+        element = item.toElement()
     47+        self.assertEquals('item', element.name)
     48+        self.assertEquals(NS_ROSTER, element.uri)
     49+        self.assertFalse(element.hasAttribute('subscription'))
     50+        self.assertFalse(element.hasAttribute('ask'))
     51+        self.assertFalse(element.hasAttribute('name'))
     52+        self.assertFalse(element.hasAttribute('approved'))
     53+        self.assertEquals(0, len(list(element.elements())))
     54+
     55+
     56+    def test_toElementMinimal(self):
     57+        item = xmppim.RosterItem(JID('user@example.org'))
     58+        element = item.toElement()
     59+        self.assertEquals(u'user@example.org', element.getAttribute('jid'))
     60+
     61+
     62+    def test_toElementSubscriptionNone(self):
     63+        item = xmppim.RosterItem(JID('user@example.org'),
     64+                                 subscriptionTo=False,
     65+                                 subscriptionFrom=False)
     66+        element = item.toElement()
     67+        self.assertIdentical(None, element.getAttribute('subscription'))
     68+
     69+
     70+    def test_toElementSubscriptionTo(self):
     71+        item = xmppim.RosterItem(JID('user@example.org'),
     72+                                 subscriptionTo=True,
     73+                                 subscriptionFrom=False)
     74+        element = item.toElement()
     75+        self.assertEquals('to', element.getAttribute('subscription'))
     76+
     77+
     78+    def test_toElementSubscriptionFrom(self):
     79+        item = xmppim.RosterItem(JID('user@example.org'),
     80+                                 subscriptionTo=False,
     81+                                 subscriptionFrom=True)
     82+        element = item.toElement()
     83+        self.assertEquals('from', element.getAttribute('subscription'))
     84+
     85+
     86+    def test_toElementSubscriptionBoth(self):
     87+        item = xmppim.RosterItem(JID('user@example.org'),
     88+                                 subscriptionTo=True,
     89+                                 subscriptionFrom=True)
     90+        element = item.toElement()
     91+        self.assertEquals('both', element.getAttribute('subscription'))
     92+
     93+
     94+    def test_toElementSubscriptionRemove(self):
     95+        item = xmppim.RosterItem(JID('user@example.org'))
     96+        item.remove = True
     97+        element = item.toElement()
     98+        self.assertEquals('remove', element.getAttribute('subscription'))
     99+
     100+
     101+    def test_toElementAsk(self):
     102+        item = xmppim.RosterItem(JID('user@example.org'))
     103+        item.pendingOut = True
     104+        element = item.toElement()
     105+        self.assertEquals('subscribe', element.getAttribute('ask'))
     106+
     107+
     108+    def test_toElementName(self):
     109+        item = xmppim.RosterItem(JID('user@example.org'),
     110+                                 name='Joe User')
     111+        element = item.toElement()
     112+        self.assertEquals(u'Joe User', element.getAttribute('name'))
     113+
     114+
     115+    def test_toElementGroups(self):
     116+        groups = set(['Friends', 'Jabber'])
     117+        item = xmppim.RosterItem(JID('user@example.org'),
     118+                                 groups=groups)
     119+
     120+        element = item.toElement()
     121+        foundGroups = set()
     122+        for child in element.elements():
     123+            if child.uri == NS_ROSTER and child.name == 'group':
     124+                foundGroups.add(unicode(child))
     125+
     126+        self.assertEqual(groups, foundGroups)
     127+
     128+
     129+    def test_toElementApproved(self):
     130+        """
     131+        A pre-approved subscription for a roster item has an 'approved' flag.
     132+        """
     133+        item = xmppim.RosterItem(JID('user@example.org'))
     134+        item.approved = True
     135+        element = item.toElement()
     136+        self.assertEquals(u'true', element.getAttribute('approved'))
     137+
     138+
     139+    def test_fromElementMinimal(self):
     140+        """
     141+        A minimal roster item has a reference to the JID of the contact.
     142+        """
     143+
     144+        xml = """
     145+            <item xmlns="jabber:iq:roster"
     146+                  jid="test@example.org"/>
     147+        """
     148+
     149+        item = xmppim.RosterItem.fromElement(parseXml(xml))
     150+        self.assertEqual(JID(u"test@example.org"), item.entity)
     151+        self.assertIdentical(None, item.name)
     152+        self.assertFalse(item.subscriptionTo)
     153+        self.assertFalse(item.subscriptionFrom)
     154+        self.assertFalse(item.pendingOut)
     155+        self.assertFalse(item.approved)
     156+        self.assertEquals(set(), item.groups)
     157+
     158+
     159+    def test_fromElementName(self):
     160+        """
     161+        A roster item may have an optional name.
     162+        """
     163+
     164+        xml = """
     165+            <item xmlns="jabber:iq:roster"
     166+                  jid="test@example.org"
     167+                  name="Test User"/>
     168+        """
     169+
     170+        item = xmppim.RosterItem.fromElement(parseXml(xml))
     171+        self.assertEqual(u"Test User", item.name)
     172+
     173+
     174+    def test_fromElementGroups(self):
     175+        """
     176+        A roster item may have one or more groups.
     177+        """
     178+
     179+        xml = """
     180+            <item xmlns="jabber:iq:roster"
     181+                  jid="test@example.org">
     182+              <group>Friends</group>
     183+              <group>Twisted</group>
     184+            </item>
     185+        """
     186+
     187+        item = xmppim.RosterItem.fromElement(parseXml(xml))
     188+        self.assertIn(u"Twisted", item.groups)
     189+        self.assertIn(u"Friends", item.groups)
     190+
     191+
     192+    def test_fromElementSubscriptionNone(self):
     193+        """
     194+        Subscription 'none' sets both attributes to False.
     195+        """
     196+
     197+        xml = """
     198+            <item xmlns="jabber:iq:roster"
     199+                  jid="test@example.org"
     200+                  subscription="none"/>
     201+        """
     202+
     203+        item = xmppim.RosterItem.fromElement(parseXml(xml))
     204+        self.assertFalse(item.remove)
     205+        self.assertFalse(item.subscriptionTo)
     206+        self.assertFalse(item.subscriptionFrom)
     207+
     208+
     209+    def test_fromElementSubscriptionTo(self):
     210+        """
     211+        Subscription 'to' sets the corresponding attribute to True.
     212+        """
     213+
     214+        xml = """
     215+            <item xmlns="jabber:iq:roster"
     216+                  jid="test@example.org"
     217+                  subscription="to"/>
     218+        """
     219+
     220+        item = xmppim.RosterItem.fromElement(parseXml(xml))
     221+        self.assertFalse(item.remove)
     222+        self.assertTrue(item.subscriptionTo)
     223+        self.assertFalse(item.subscriptionFrom)
     224+
     225+
     226+    def test_fromElementSubscriptionFrom(self):
     227+        """
     228+        Subscription 'from' sets the corresponding attribute to True.
     229+        """
     230+
     231+        xml = """
     232+            <item xmlns="jabber:iq:roster"
     233+                  jid="test@example.org"
     234+                  subscription="from"/>
     235+        """
     236+
     237+        item = xmppim.RosterItem.fromElement(parseXml(xml))
     238+        self.assertFalse(item.remove)
     239+        self.assertFalse(item.subscriptionTo)
     240+        self.assertTrue(item.subscriptionFrom)
     241+
     242+
     243+    def test_fromElementSubscriptionBoth(self):
     244+        """
     245+        Subscription 'both' sets both attributes to True.
     246+        """
     247+
     248+        xml = """
     249+            <item xmlns="jabber:iq:roster"
     250+                  jid="test@example.org"
     251+                  subscription="both"/>
     252+        """
     253+
     254+        item = xmppim.RosterItem.fromElement(parseXml(xml))
     255+        self.assertFalse(item.remove)
     256+        self.assertTrue(item.subscriptionTo)
     257+        self.assertTrue(item.subscriptionFrom)
     258+
     259+
     260+    def test_fromElementSubscriptionRemove(self):
     261+        """
     262+        Subscription 'remove' sets the remove attribute.
     263+        """
     264+
     265+        xml = """
     266+            <item xmlns="jabber:iq:roster"
     267+                  jid="test@example.org"
     268+                  subscription="remove"/>
     269+        """
     270+
     271+        item = xmppim.RosterItem.fromElement(parseXml(xml))
     272+        self.assertTrue(item.remove)
     273+
     274+
     275+    def test_fromElementPendingOut(self):
     276+        """
     277+        The ask attribute, if set to 'subscription', means pending out.
     278+        """
     279+
     280+        xml = """
     281+            <item xmlns="jabber:iq:roster"
     282+                  jid="test@example.org"
     283+                  ask="subscribe"/>
     284+        """
     285+
     286+        item = xmppim.RosterItem.fromElement(parseXml(xml))
     287+        self.assertTrue(item.pendingOut)
     288+
     289+
     290+    def test_fromElementApprovedTrue(self):
     291+        """
     292+        The approved attribute (true) signals a pre-approved subscription.
     293+        """
     294+
     295+        xml = """
     296+            <item xmlns="jabber:iq:roster"
     297+                  jid="test@example.org"
     298+                  approved="true"/>
     299+        """
     300+
     301+        item = xmppim.RosterItem.fromElement(parseXml(xml))
     302+        self.assertTrue(item.approved)
     303+
     304+
     305+    def test_fromElementApproved1(self):
     306+        """
     307+        The approved attribute (1) signals a pre-approved subscription.
     308+        """
     309+
     310+        xml = """
     311+            <item xmlns="jabber:iq:roster"
     312+                  jid="test@example.org"
     313+                  approved="1"/>
     314+        """
     315+
     316+        item = xmppim.RosterItem.fromElement(parseXml(xml))
     317+        self.assertTrue(item.approved)
     318+
     319+
     320+    def test_jidDeprecationGet(self):
     321+        """
     322+        Getting the jid attribute works as entity and warns deprecation.
     323+        """
     324+        item = xmppim.RosterItem(JID('user@example.org'))
     325+        entity = self.assertWarns(DeprecationWarning,
     326+                                  "wokkel.xmppim.RosterItem.jid is deprecated. "
     327+                                  "Use RosterItem.entity instead.",
     328+                                  xmppim.__file__,
     329+                                  getattr, item, 'jid')
     330+        self.assertIdentical(entity, item.entity)
     331+
     332+
     333+    def test_jidDeprecationSet(self):
     334+        """
     335+        Setting the jid attribute works as entity and warns deprecation.
     336+        """
     337+        item = xmppim.RosterItem(JID('user@example.org'))
     338+        self.assertWarns(DeprecationWarning,
     339+                         "wokkel.xmppim.RosterItem.jid is deprecated. "
     340+                         "Use RosterItem.entity instead.",
     341+                         xmppim.__file__,
     342+                         setattr, item, 'jid',
     343+                         JID('other@example.org'))
     344+        self.assertEquals(JID('other@example.org'), item.entity)
     345+
     346+
     347+    def test_askDeprecationGet(self):
     348+        """
     349+        Getting the ask attribute works as entity and warns deprecation.
     350+        """
     351+        item = xmppim.RosterItem(JID('user@example.org'))
     352+        item.pendingOut = True
     353+        ask = self.assertWarns(DeprecationWarning,
     354+                               "wokkel.xmppim.RosterItem.ask is deprecated. "
     355+                               "Use RosterItem.pendingOut instead.",
     356+                               xmppim.__file__,
     357+                               getattr, item, 'ask')
     358+        self.assertTrue(ask)
     359+
     360+
     361+    def test_askDeprecationSet(self):
     362+        """
     363+        Setting the ask attribute works as entity and warns deprecation.
     364+        """
     365+        item = xmppim.RosterItem(JID('user@example.org'))
     366+        self.assertWarns(DeprecationWarning,
     367+                         "wokkel.xmppim.RosterItem.ask is deprecated. "
     368+                         "Use RosterItem.pendingOut instead.",
     369+                         xmppim.__file__,
     370+                         setattr, item, 'ask',
     371+                         True)
     372+        self.assertTrue(item.pendingOut)
     373+
     374+
     375+
     376+class RosterClientProtocolTest(unittest.TestCase, TestableRequestHandlerMixin):
     377     """
     378     Tests for L{xmppim.RosterClientProtocol}.
     379     """
     380 
     381     def setUp(self):
     382         self.stub = XmlStreamStub()
     383-        self.protocol = xmppim.RosterClientProtocol()
     384-        self.protocol.xmlstream = self.stub.xmlstream
     385-        self.protocol.connectionInitialized()
     386+        self.service = xmppim.RosterClientProtocol()
     387+        self.service.makeConnection(self.stub.xmlstream)
     388+        self.service.connectionInitialized()
     389 
     390 
     391     def test_removeItem(self):
     392         """
     393         Removing a roster item is setting an item with subscription C{remove}.
     394         """
     395-        d = self.protocol.removeItem(JID('test@example.org'))
     396+        d = self.service.removeItem(JID('test@example.org'))
     397 
     398         # Inspect outgoing iq request
     399 
     400@@ -479,10 +817,128 @@
     401         self.assertEquals(1, len(children))
     402         child = children[0]
     403         self.assertEquals('test@example.org', child['jid'])
     404-        self.assertEquals('remove', child['subscription'])
     405+        self.assertEquals('remove', child.getAttribute('subscription'))
     406 
     407         # Fake successful response
     408 
     409         response = toResponse(iq, 'result')
     410         self.stub.send(response)
     411         return d
     412+
     413+
     414+    def test_getRoster(self):
     415+        def cb(roster):
     416+            self.assertIn(JID('user@example.org'), roster)
     417+
     418+
     419+        d = self.service.getRoster()
     420+        d.addCallback(cb)
     421+
     422+        # Inspect outgoing iq request
     423+
     424+        iq = self.stub.output[-1]
     425+        self.assertEquals('get', iq.getAttribute('type'))
     426+        self.assertNotIdentical(None, iq.query)
     427+        self.assertEquals(NS_ROSTER, iq.query.uri)
     428+
     429+        # Fake successful response
     430+        response = toResponse(iq, 'result')
     431+        query = response.addElement((NS_ROSTER, 'query'))
     432+        item = query.addElement('item')
     433+        item['jid'] = 'user@example.org'
     434+
     435+        self.stub.send(response)
     436+        return d
     437+
     438+
     439+    def test_onRosterSet(self):
     440+        """
     441+        A roster push causes onRosterSet to be called with the parsed item.
     442+        """
     443+        xml = """
     444+          <iq type='set'>
     445+            <query xmlns='jabber:iq:roster'>
     446+              <item jid='user@example.org'/>
     447+            </query>
     448+          </iq>
     449+        """
     450+
     451+        items = []
     452+
     453+        def onRosterSet(item):
     454+            items.append(item)
     455+
     456+        def cb(result):
     457+            self.assertEquals(1, len(items))
     458+            self.assertEquals(JID('user@example.org'), items[0].entity)
     459+
     460+        self.service.onRosterSet = onRosterSet
     461+
     462+        d = self.assertWarns(DeprecationWarning,
     463+                             "wokkel.xmppim.RosterClientProtocol.onRosterSet "
     464+                             "is deprecated. "
     465+                             "Use RosterClientProtocol.pushReceived instead.",
     466+                             xmppim.__file__,
     467+                             self.handleRequest, xml)
     468+        d.addCallback(cb)
     469+        return d
     470+
     471+
     472+    def test_onRosterRemove(self):
     473+        """
     474+        A roster push causes onRosterSet to be called with the parsed item.
     475+        """
     476+        xml = """
     477+          <iq type='set'>
     478+            <query xmlns='jabber:iq:roster'>
     479+              <item jid='user@example.org' subscription='remove'/>
     480+            </query>
     481+          </iq>
     482+        """
     483+
     484+        entities = []
     485+
     486+        def onRosterRemove(entity):
     487+            entities.append(entity)
     488+
     489+        def cb(result):
     490+            self.assertEquals([JID('user@example.org')], entities)
     491+
     492+        self.service.onRosterRemove = onRosterRemove
     493+
     494+        d = self.assertWarns(DeprecationWarning,
     495+                             "wokkel.xmppim.RosterClientProtocol.onRosterRemove "
     496+                             "is deprecated. "
     497+                             "Use RosterClientProtocol.pushReceived instead.",
     498+                             xmppim.__file__,
     499+                             self.handleRequest, xml)
     500+        d.addCallback(cb)
     501+        return d
     502+
     503+
     504+    def test_pushReceived(self):
     505+        """
     506+        A roster push causes pushReceived to be called with the parsed item.
     507+        """
     508+        xml = """
     509+          <iq type='set'>
     510+            <query xmlns='jabber:iq:roster'>
     511+              <item jid='user@example.org'/>
     512+            </query>
     513+          </iq>
     514+        """
     515+
     516+        items = []
     517+
     518+        def pushReceived(item):
     519+            items.append(item)
     520+
     521+        def cb(result):
     522+            self.assertEquals(1, len(items), "pushReceived was not called")
     523+            self.assertEquals(JID('user@example.org'), items[0].entity)
     524+
     525+        self.service.pushReceived = pushReceived
     526+
     527+        d = self.handleRequest(xml)
     528+        d.addCallback(cb)
     529+        return d
     530diff --git a/wokkel/xmppim.py b/wokkel/xmppim.py
     531--- a/wokkel/xmppim.py
     532+++ b/wokkel/xmppim.py
     533@@ -7,21 +7,28 @@
    20534 XMPP IM protocol support.
    21535 
     
    27541 """
    28542 
     543+import warnings
     544+
    29545+from twisted.internet import defer
    30546+from twisted.words.protocols.jabber import error
    31  from twisted.words.protocols.jabber import jid
     547 from twisted.words.protocols.jabber.jid import JID
    32548 from twisted.words.xish import domish
    33549 
     
    44560+
    45561+
    46  class BasePresence(Stanza):
    47      """
    48      Stanza of kind presence.
    49 @@ -377,8 +382,8 @@
     562 class Presence(domish.Element):
     563     def __init__(self, to=None, type=None):
     564         domish.Element.__init__(self, (None, "presence"))
     565@@ -605,8 +612,8 @@
    50566 
    51567     This represents one contact from an XMPP contact list known as roster.
    52568 
    53569-    @ivar jid: The JID of the contact.
    54 -    @type jid: L{jid.JID}
     570-    @type jid: L{JID}
    55571+    @ivar entity: The JID of the contact.
    56 +    @type entity: L{jid.JID}
     572+    @type entity: L{JID}
    57573     @ivar name: The optional associated nickname for this contact.
    58574     @type name: C{unicode}
    59575     @ivar subscriptionTo: Subscription state to contact's presence. If C{True},
    60 @@ -388,47 +393,99 @@
     576@@ -616,45 +623,137 @@
    61577     @ivar subscriptionFrom: Contact's subscription state. If C{True}, the
    62578                             contact is subscribed to the presence information
     
    72588                   represented by an opaque identifier of type C{unicode}.
    73589     @type groups: C{set}
     590+    @ivar approved: Signals pre-approved subscription.
     591+    @type approved: C{bool}
     592+    @ivar remove: Signals roster item removal.
     593+    @type remove: C{bool}
    74594     """
    75595 
     
    94614+        self.groups = groups or set()
    95615 
     616-class RosterClientProtocol(XMPPHandler):
    96617+        self.pendingOut = False
    97618+        self.approved = False
    98619+        self.remove = False
    99  
    100 -class RosterClientProtocol(XMPPHandler):
     620+
     621+
     622+    def __getJID(self):
     623+        warnings.warn(
     624+            "wokkel.xmppim.RosterItem.jid is deprecated. "
     625+            "Use RosterItem.entity instead.",
     626+            DeprecationWarning)
     627+        return self.entity
     628+
     629+
     630+    def __setJID(self, value):
     631+        warnings.warn(
     632+            "wokkel.xmppim.RosterItem.jid is deprecated. "
     633+            "Use RosterItem.entity instead.",
     634+            DeprecationWarning)
     635+        self.entity = value
     636+
     637+
     638+    jid = property(__getJID, __setJID, doc="""
     639+            JID of the contact. Deprecated in favour of C{entity}.""")
     640+
     641+
     642+    def __getAsk(self):
     643+        warnings.warn(
     644+            "wokkel.xmppim.RosterItem.ask is deprecated. "
     645+            "Use RosterItem.pendingOut instead.",
     646+            DeprecationWarning)
     647+        return self.pendingOut
     648+
     649+
     650+    def __setAsk(self, value):
     651+        warnings.warn(
     652+            "wokkel.xmppim.RosterItem.ask is deprecated. "
     653+            "Use RosterItem.pendingOut instead.",
     654+            DeprecationWarning)
     655+        self.pendingOut = value
     656+
     657+
     658+    ask = property(__getAsk, __setAsk, doc="""
     659+            Pending out subscription. Deprecated in favour of C{pendingOut}.""")
     660+
    101661+
    102662+    def toElement(self):
     
    131691+    @classmethod
    132692+    def fromElement(Class, element):
    133 +        entity = jid.internJID(element['jid'])
     693+        entity = JID(element['jid'])
    134694+        item = Class(entity)
    135695+        subscription = element.getAttribute('subscription')
     
    149709+
    150710+
    151 +class RosterPushIgnored(Exception):
    152 +    """
    153 +    Raised when this entity doesn't want to accept/trust a roster push.
    154 +    """
    155 +
    156 +
    157 +
    158711+class RosterClientProtocol(XMPPHandler, IQHandlerMixin):
    159712     """
     
    167720-        ROSTER_SET = "/iq[@type='set']/query[@xmlns='%s']" % NS_ROSTER
    168721-        self.xmlstream.addObserver(ROSTER_SET, self._onRosterSet)
    169 -
    170 -
     722+        self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest)
     723 
    171724-    def _parseRosterItem(self, element):
    172 -        entity = jid.internJID(element['jid'])
    173 -        item = RosterItem(entity)
     725-        jid = JID(element['jid'])
     726-        item = RosterItem(jid)
    174727-        item.name = element.getAttribute('name')
    175728-        subscription = element.getAttribute('subscription')
     
    182735-
    183736-        return item
    184 +        self.xmlstream.addObserver(XPATH_ROSTER_SET, self.handleRequest)
    185  
    186737 
    187738     def getRoster(self):
    188 @@ -443,8 +500,8 @@
     739         """
     740@@ -668,8 +767,8 @@
    189741             roster = {}
    190742             for element in domish.generateElementsQNamed(result.query.children,
     
    197749             return roster
    198750 
    199 @@ -465,32 +522,36 @@
     751@@ -690,42 +789,48 @@
    200752         """
    201753         iq = IQ(self.xmlstream, 'set')
     
    214766-           iq.hasAttribute('from') and iq['from'] != self.xmlstream:
    215767-            return
    216 +        def eb(failure):
    217 +            failure.trap(RosterPushIgnored)
    218 +            raise error.StanzaError('service-unavailable')
     768+        item = RosterItem.fromElement(iq.query.item)
    219769 
    220770-        iq.handled = True
    221 +        item = RosterItem.fromElement(iq.query.item)
     771+        d = defer.maybeDeferred(self.pushReceived, item)
     772+        return d
    222773 
    223774-        itemElement = iq.query.item
    224 +        if item.remove:
    225 +            d = defer.maybeDeferred(self.onRosterRemove, item.entity)
    226 +        else:
    227 +            d = defer.maybeDeferred(self.onRosterSet, item)
    228775 
    229776-        if unicode(itemElement['subscription']) == 'remove':
    230 -            self.onRosterRemove(jid.internJID(itemElement['jid']))
     777-            self.onRosterRemove(JID(itemElement['jid']))
    231778-        else:
    232779-            item = self._parseRosterItem(iq.query.item)
    233780-            self.onRosterSet(item)
    234 +        d.addErrback(eb)
    235 +        return d
    236  
    237  
    238      def onRosterSet(self, item):
    239          """
    240          Called when a roster push for a new or update item was received.
    241  
    242 +        Raise L{RosterPushIgnored} when not accepting this roster push
    243 +        (directly or via Deferred). This will result in a
    244 +        L{'service-unavailable'} error being sent in return.
    245 +
     781+    def pushReceived(self, item):
     782+        """
     783+        Called when a roster push was received.
     784 
     785-    def onRosterSet(self, item):
     786-        """
     787-        Called when a roster push for a new or update item was received.
     788+        Override this to handle roster pushes.
     789+
     790+        For backwards compatibility, the default implementation calls
     791+        the deprecated C{onRosterSet} or C{onRosterRemove} if defined on
     792+        C{self}.
     793 
    246794         @param item: The pushed roster item.
    247795         @type item: L{RosterItem}
    248796         """
    249 @@ -500,6 +561,10 @@
    250          """
    251          Called when a roster push for the removal of an item was received.
    252  
    253 +        Raise L{RosterPushIgnored} when not accepting this roster push
    254 +        (directly or via Deferred). This will result in a
    255 +        L{'service-unavailable'} error being sent in return.
    256 +
    257          @param entity: The entity for which the roster item has been removed.
    258          @type entity: L{jid.JID}
    259          """
    260 diff -r a3c50205821b wokkel/test/test_im.py
    261 --- a/wokkel/test/test_im.py    Wed Mar 28 13:13:06 2012 +0200
    262 +++ b/wokkel/test/test_im.py    Wed Mar 28 13:17:05 2012 +0200
    263 @@ -7,13 +7,14 @@
    264  
    265  from twisted.internet import defer
    266  from twisted.trial import unittest
    267 +from twisted.words.protocols.jabber import error
    268  from twisted.words.protocols.jabber.jid import JID
    269  from twisted.words.protocols.jabber.xmlstream import toResponse
    270  from twisted.words.xish import domish, utility
    271  
    272  from wokkel import im
    273  from wokkel.generic import ErrorStanza, parseXml
    274 -from wokkel.test.helpers import XmlStreamStub
    275 +from wokkel.test.helpers import TestableRequestHandlerMixin, XmlStreamStub
    276  
    277  NS_XML = 'http://www.w3.org/XML/1998/namespace'
    278  NS_ROSTER = 'jabber:iq:roster'
    279 @@ -389,23 +390,305 @@
    280  
    281  
    282  
    283 -class RosterClientProtocolTest(unittest.TestCase):
    284 +class RosterItemTest(unittest.TestCase):
    285 +    """
    286 +    Tests for L{im.RosterItem}.
    287 +    """
    288 +
    289 +    def test_toElement(self):
    290 +        item = im.RosterItem(JID('user@example.org'))
    291 +        element = item.toElement()
    292 +        self.assertEquals('item', element.name)
    293 +        self.assertEquals(NS_ROSTER, element.uri)
    294 +        self.assertFalse(element.hasAttribute('subscription'))
    295 +        self.assertFalse(element.hasAttribute('ask'))
    296 +        self.assertFalse(element.hasAttribute('name'))
    297 +        self.assertFalse(element.hasAttribute('approved'))
    298 +        self.assertEquals(0, len(list(element.elements())))
    299 +
    300 +
    301 +    def test_toElementMinimal(self):
    302 +        item = im.RosterItem(JID('user@example.org'))
    303 +        element = item.toElement()
    304 +        self.assertEquals(u'user@example.org', element.getAttribute('jid'))
    305 +
    306 +
    307 +    def test_toElementSubscriptionNone(self):
    308 +        item = im.RosterItem(JID('user@example.org'),
    309 +                                 subscriptionTo=False,
    310 +                                 subscriptionFrom=False)
    311 +        element = item.toElement()
    312 +        self.assertIdentical(None, element.getAttribute('subscription'))
    313 +
    314 +
    315 +    def test_toElementSubscriptionTo(self):
    316 +        item = im.RosterItem(JID('user@example.org'),
    317 +                                 subscriptionTo=True,
    318 +                                 subscriptionFrom=False)
    319 +        element = item.toElement()
    320 +        self.assertEquals('to', element.getAttribute('subscription'))
    321 +
    322 +
    323 +    def test_toElementSubscriptionFrom(self):
    324 +        item = im.RosterItem(JID('user@example.org'),
    325 +                                 subscriptionTo=False,
    326 +                                 subscriptionFrom=True)
    327 +        element = item.toElement()
    328 +        self.assertEquals('from', element.getAttribute('subscription'))
    329 +
    330 +
    331 +    def test_toElementSubscriptionBoth(self):
    332 +        item = im.RosterItem(JID('user@example.org'),
    333 +                                 subscriptionTo=True,
    334 +                                 subscriptionFrom=True)
    335 +        element = item.toElement()
    336 +        self.assertEquals('both', element.getAttribute('subscription'))
    337 +
    338 +
    339 +    def test_toElementSubscriptionRemove(self):
    340 +        item = im.RosterItem(JID('user@example.org'))
    341 +        item.remove = True
    342 +        element = item.toElement()
    343 +        self.assertEquals('remove', element.getAttribute('subscription'))
    344 +
    345 +
    346 +    def test_toElementAsk(self):
    347 +        item = im.RosterItem(JID('user@example.org'))
    348 +        item.pendingOut = True
    349 +        element = item.toElement()
    350 +        self.assertEquals('subscribe', element.getAttribute('ask'))
    351 +
    352 +
    353 +    def test_toElementName(self):
    354 +        item = im.RosterItem(JID('user@example.org'),
    355 +                                 name='Joe User')
    356 +        element = item.toElement()
    357 +        self.assertEquals(u'Joe User', element.getAttribute('name'))
    358 +
    359 +
    360 +    def test_toElementGroups(self):
    361 +        groups = set(['Friends', 'Jabber'])
    362 +        item = im.RosterItem(JID('user@example.org'),
    363 +                                 groups=groups)
    364 +
    365 +        element = item.toElement()
    366 +        foundGroups = set()
    367 +        for child in element.elements():
    368 +            if child.uri == NS_ROSTER and child.name == 'group':
    369 +                foundGroups.add(unicode(child))
    370 +
    371 +        self.assertEqual(groups, foundGroups)
    372 +
    373 +
    374 +    def test_toElementApproved(self):
    375 +        """
    376 +        A pre-approved subscription for a roster item has an 'approved' flag.
    377 +        """
    378 +        item = im.RosterItem(JID('user@example.org'))
    379 +        item.approved = True
    380 +        element = item.toElement()
    381 +        self.assertEquals(u'true', element.getAttribute('approved'))
    382 +
    383 +
    384 +    def test_fromElementMinimal(self):
    385 +        """
    386 +        A minimal roster item has a reference to the JID of the contact.
    387 +        """
    388 +
    389 +        xml = """
    390 +            <item xmlns="jabber:iq:roster"
    391 +                  jid="test@example.org"/>
    392 +        """
    393 +
    394 +        item = im.RosterItem.fromElement(parseXml(xml))
    395 +        self.assertEqual(JID(u"test@example.org"), item.entity)
    396 +        self.assertIdentical(None, item.name)
    397 +        self.assertFalse(item.subscriptionTo)
    398 +        self.assertFalse(item.subscriptionFrom)
    399 +        self.assertFalse(item.pendingOut)
    400 +        self.assertFalse(item.approved)
    401 +        self.assertEquals(set(), item.groups)
    402 +
    403 +
    404 +    def test_fromElementName(self):
    405 +        """
    406 +        A roster item may have an optional name.
    407 +        """
    408 +
    409 +        xml = """
    410 +            <item xmlns="jabber:iq:roster"
    411 +                  jid="test@example.org"
    412 +                  name="Test User"/>
    413 +        """
    414 +
    415 +        item = im.RosterItem.fromElement(parseXml(xml))
    416 +        self.assertEqual(u"Test User", item.name)
    417 +
    418 +
    419 +    def test_fromElementGroups(self):
    420 +        """
    421 +        A roster item may have one or more groups.
    422 +        """
    423 +
    424 +        xml = """
    425 +            <item xmlns="jabber:iq:roster"
    426 +                  jid="test@example.org">
    427 +              <group>Friends</group>
    428 +              <group>Twisted</group>
    429 +            </item>
    430 +        """
    431 +
    432 +        item = im.RosterItem.fromElement(parseXml(xml))
    433 +        self.assertIn(u"Twisted", item.groups)
    434 +        self.assertIn(u"Friends", item.groups)
    435 +
    436 +
    437 +    def test_fromElementSubscriptionNone(self):
    438 +        """
    439 +        Subscription 'none' sets both attributes to False.
    440 +        """
    441 +
    442 +        xml = """
    443 +            <item xmlns="jabber:iq:roster"
    444 +                  jid="test@example.org"
    445 +                  subscription="none"/>
    446 +        """
    447 +
    448 +        item = im.RosterItem.fromElement(parseXml(xml))
    449 +        self.assertFalse(item.remove)
    450 +        self.assertFalse(item.subscriptionTo)
    451 +        self.assertFalse(item.subscriptionFrom)
    452 +
    453 +
    454 +    def test_fromElementSubscriptionTo(self):
    455 +        """
    456 +        Subscription 'to' sets the corresponding attribute to True.
    457 +        """
    458 +
    459 +        xml = """
    460 +            <item xmlns="jabber:iq:roster"
    461 +                  jid="test@example.org"
    462 +                  subscription="to"/>
    463 +        """
    464 +
    465 +        item = im.RosterItem.fromElement(parseXml(xml))
    466 +        self.assertFalse(item.remove)
    467 +        self.assertTrue(item.subscriptionTo)
    468 +        self.assertFalse(item.subscriptionFrom)
    469 +
    470 +
    471 +    def test_fromElementSubscriptionFrom(self):
    472 +        """
    473 +        Subscription 'from' sets the corresponding attribute to True.
    474 +        """
    475 +
    476 +        xml = """
    477 +            <item xmlns="jabber:iq:roster"
    478 +                  jid="test@example.org"
    479 +                  subscription="from"/>
    480 +        """
    481 +
    482 +        item = im.RosterItem.fromElement(parseXml(xml))
    483 +        self.assertFalse(item.remove)
    484 +        self.assertFalse(item.subscriptionTo)
    485 +        self.assertTrue(item.subscriptionFrom)
    486 +
    487 +
    488 +    def test_fromElementSubscriptionBoth(self):
    489 +        """
    490 +        Subscription 'both' sets both attributes to True.
    491 +        """
    492 +
    493 +        xml = """
    494 +            <item xmlns="jabber:iq:roster"
    495 +                  jid="test@example.org"
    496 +                  subscription="both"/>
    497 +        """
    498 +
    499 +        item = im.RosterItem.fromElement(parseXml(xml))
    500 +        self.assertFalse(item.remove)
    501 +        self.assertTrue(item.subscriptionTo)
    502 +        self.assertTrue(item.subscriptionFrom)
    503 +
    504 +
    505 +    def test_fromElementSubscriptionRemove(self):
    506 +        """
    507 +        Subscription 'remove' sets the remove attribute.
    508 +        """
    509 +
    510 +        xml = """
    511 +            <item xmlns="jabber:iq:roster"
    512 +                  jid="test@example.org"
    513 +                  subscription="remove"/>
    514 +        """
    515 +
    516 +        item = im.RosterItem.fromElement(parseXml(xml))
    517 +        self.assertTrue(item.remove)
    518 +
    519 +
    520 +    def test_fromElementPendingOut(self):
    521 +        """
    522 +        The ask attribute, if set to 'subscription', means pending out.
    523 +        """
    524 +
    525 +        xml = """
    526 +            <item xmlns="jabber:iq:roster"
    527 +                  jid="test@example.org"
    528 +                  ask="subscribe"/>
    529 +        """
    530 +
    531 +        item = im.RosterItem.fromElement(parseXml(xml))
    532 +        self.assertTrue(item.pendingOut)
    533 +
    534 +
    535 +    def test_fromElementApprovedTrue(self):
    536 +        """
    537 +        The approved attribute (true) signals a pre-approved subscription.
    538 +        """
    539 +
    540 +        xml = """
    541 +            <item xmlns="jabber:iq:roster"
    542 +                  jid="test@example.org"
    543 +                  approved="true"/>
    544 +        """
    545 +
    546 +        item = im.RosterItem.fromElement(parseXml(xml))
    547 +        self.assertTrue(item.approved)
    548 +
    549 +
    550 +    def test_fromElementApproved1(self):
    551 +        """
    552 +        The approved attribute (1) signals a pre-approved subscription.
    553 +        """
    554 +
    555 +        xml = """
    556 +            <item xmlns="jabber:iq:roster"
    557 +                  jid="test@example.org"
    558 +                  approved="1"/>
    559 +        """
    560 +
    561 +        item = im.RosterItem.fromElement(parseXml(xml))
    562 +        self.assertTrue(item.approved)
    563 +
    564 +
    565 +
    566 +class RosterClientProtocolTest(unittest.TestCase, TestableRequestHandlerMixin):
    567      """
    568      Tests for L{im.RosterClientProtocol}.
    569      """
    570  
    571      def setUp(self):
    572          self.stub = XmlStreamStub()
    573 -        self.protocol = im.RosterClientProtocol()
    574 -        self.protocol.xmlstream = self.stub.xmlstream
    575 -        self.protocol.connectionInitialized()
    576 +        self.service = im.RosterClientProtocol()
    577 +        self.service.makeConnection(self.stub.xmlstream)
    578 +        self.service.connectionInitialized()
    579  
    580  
    581      def test_removeItem(self):
    582          """
    583          Removing a roster item is setting an item with subscription C{remove}.
    584          """
    585 -        d = self.protocol.removeItem(JID('test@example.org'))
    586 +        d = self.service.removeItem(JID('test@example.org'))
    587  
    588          # Inspect outgoing iq request
    589  
    590 @@ -419,10 +702,117 @@
    591          self.assertEquals(1, len(children))
    592          child = children[0]
    593          self.assertEquals('test@example.org', child['jid'])
    594 -        self.assertEquals('remove', child['subscription'])
    595 +        self.assertEquals('remove', child.getAttribute('subscription'))
    596  
    597          # Fake successful response
    598  
    599          response = toResponse(iq, 'result')
    600          self.stub.send(response)
    601          return d
    602 +
    603 +
    604 +    def test_getRoster(self):
    605 +        def cb(roster):
    606 +            self.assertIn(JID('user@example.org'), roster)
    607 +
    608 +
    609 +        d = self.service.getRoster()
    610 +        d.addCallback(cb)
    611 +
    612 +        # Inspect outgoing iq request
    613 +
    614 +        iq = self.stub.output[-1]
    615 +        self.assertEquals('get', iq.getAttribute('type'))
    616 +        self.assertNotIdentical(None, iq.query)
    617 +        self.assertEquals(NS_ROSTER, iq.query.uri)
    618 +
    619 +        # Fake successful response
    620 +        response = toResponse(iq, 'result')
    621 +        query = response.addElement((NS_ROSTER, 'query'))
    622 +        item = query.addElement('item')
    623 +        item['jid'] = 'user@example.org'
    624 +
    625 +        self.stub.send(response)
    626 +        return d
    627 +
    628 +
    629 +    def test_onRosterSet(self):
    630 +        """
    631 +        A roster push causes onRosterSet to be called with the parsed item.
    632 +        """
    633 +        xml = """
    634 +          <iq type='set'>
    635 +            <query xmlns='jabber:iq:roster'>
    636 +              <item jid='user@example.org'/>
    637 +            </query>
    638 +          </iq>
    639 +        """
    640 +
    641 +        items = []
    642 +
    643 +        def onRosterSet(item):
    644 +            items.append(item)
    645 +
    646 +        def cb(result):
    647 +            self.assertEquals(1, len(items))
    648 +            self.assertEquals(JID('user@example.org'), items[0].entity)
    649 +
    650 +        self.service.onRosterSet = onRosterSet
    651 +
    652 +        d = self.handleRequest(xml)
    653 +        d.addCallback(cb)
    654 +        return d
    655 +
    656 +
    657 +    def test_onRosterSetUntrusted(self):
    658 +        """
    659 +        Roster pushes from untrusted sources will be not be handled.
    660 +        """
    661 +        xml = """
    662 +          <iq type='set' from='bad@example.org'>
    663 +            <query xmlns='jabber:iq:roster'>
    664 +              <item jid='user@example.org'/>
    665 +            </query>
    666 +          </iq>
    667 +        """
    668 +
    669 +        def onRosterSet(item):
    670 +            raise im.RosterPushIgnored()
    671 +
    672 +        def cb(result):
    673 +            self.assertEquals('service-unavailable', result.condition)
    674 +
    675 +        self.service.onRosterSet = onRosterSet
    676 +
    677 +        d = self.handleRequest(xml)
    678 +        self.assertFailure(d, error.StanzaError)
    679 +        d.addCallback(cb)
    680 +        return d
    681 +
    682 +
    683 +    def test_onRosterRemove(self):
    684 +        """
    685 +        A roster push causes onRosterSet to be called with the parsed item.
    686 +        """
    687 +        xml = """
    688 +          <iq type='set'>
    689 +            <query xmlns='jabber:iq:roster'>
    690 +              <item jid='user@example.org' subscription='remove'/>
    691 +            </query>
    692 +          </iq>
    693 +        """
    694 +
    695 +        entities = []
    696 +
    697 +        def onRosterRemove(entity):
    698 +            entities.append(entity)
    699 +
    700 +        def cb(result):
    701 +            self.assertEquals([JID('user@example.org')], entities)
    702 +
    703 +        self.service.onRosterRemove = onRosterRemove
    704 +
    705 +        d = self.handleRequest(xml)
    706 +        d.addCallback(cb)
    707 +        return d
    708 +
     797-
     798-    def onRosterRemove(self, entity):
     799-        """
     800-        Called when a roster push for the removal of an item was received.
     801-
     802-        @param entity: The entity for which the roster item has been removed.
     803-        @type entity: L{JID}
     804-        """
     805+        if item.remove:
     806+            if hasattr(self, 'onRosterRemove'):
     807+                warnings.warn(
     808+                    "wokkel.xmppim.RosterClientProtocol.onRosterRemove "
     809+                    "is deprecated. "
     810+                    "Use RosterClientProtocol.pushReceived instead.",
     811+                    DeprecationWarning)
     812+                return defer.maybeDeferred(self.onRosterRemove, item.entity)
     813+        else:
     814+            if hasattr(self, 'onRosterSet'):
     815+                warnings.warn(
     816+                    "wokkel.xmppim.RosterClientProtocol.onRosterSet "
     817+                    "is deprecated. "
     818+                    "Use RosterClientProtocol.pushReceived instead.",
     819+                    DeprecationWarning)
     820+                return defer.maybeDeferred(self.onRosterSet, item)
     821 
     822 
     823 
Note: See TracChangeset for help on using the changeset viewer.