def test_repr(self): s = stanza.IQ(from_=TEST_FROM, to=TEST_TO, id_="someid", type_=structs.IQType.ERROR) s.error = stanza.Error() self.assertEqual( "<iq from='*****@*****.**' to='*****@*****.**'" " id='someid' type=<IQType.ERROR: 'error'>" " error=<undefined-condition type=<ErrorType.CANCEL: 'cancel'>>>", repr(s)) s = stanza.IQ(from_=TEST_FROM, to=TEST_TO, id_="someid", type_=structs.IQType.RESULT) s.payload = TestPayload() self.assertEqual( "<iq from='*****@*****.**' to='*****@*****.**'" " id='someid' type=<IQType.RESULT: 'result'>" " data=foobar>", repr(s)) s = stanza.IQ(from_=TEST_FROM, to=TEST_TO, id_="someid", type_=structs.IQType.RESULT) self.assertEqual( "<iq from='*****@*****.**' to='*****@*****.**'" " id='someid' type=<IQType.RESULT: 'result'>>", repr(s))
def test_groups_update_fires_events(self): request = roster_xso.Query(items=[ roster_xso.Item( jid=self.user1, groups=[roster_xso.Group(name="group4")], ) ]) added_cb = unittest.mock.Mock() added_cb.return_value = False removed_cb = unittest.mock.Mock() removed_cb.return_value = False with contextlib.ExitStack() as stack: stack.enter_context( self.s.on_entry_added_to_group.context_connect(added_cb)) stack.enter_context( self.s.on_entry_removed_from_group.context_connect(removed_cb)) run_coroutine( self.s.handle_roster_push( stanza.IQ(structs.IQType.SET, payload=request))) self.assertSequenceEqual([ unittest.mock.call(self.s.items[self.user1], "group4"), ], added_cb.mock_calls) self.assertIn(unittest.mock.call(self.s.items[self.user1], "group1"), removed_cb.mock_calls) self.assertIn(unittest.mock.call(self.s.items[self.user1], "group3"), removed_cb.mock_calls)
def send_and_decode_info_query(self, jid, node): request_iq = stanza.IQ(to=jid, type_=structs.IQType.GET) request_iq.payload = disco_xso.InfoQuery(node=node) response = yield from self.client.send(request_iq) return response
def test_item_removal_fixes_groups(self): request = roster_xso.Query( items=[roster_xso.Item( jid=self.user1, subscription="remove", )]) added_cb = unittest.mock.Mock() added_cb.return_value = False removed_cb = unittest.mock.Mock() removed_cb.return_value = False with contextlib.ExitStack() as stack: stack.enter_context( self.s.on_entry_added_to_group.context_connect(added_cb)) stack.enter_context( self.s.on_entry_removed_from_group.context_connect(removed_cb)) run_coroutine( self.s.handle_roster_push( stanza.IQ(structs.IQType.SET, payload=request))) self.assertSequenceEqual([], added_cb.mock_calls) self.assertSequenceEqual([], removed_cb.mock_calls) self.assertSetEqual({"group1", "group2"}, set(self.s.groups.keys())) self.assertSetEqual({self.s.items[self.user2]}, self.s.groups["group1"]) self.assertSetEqual({self.s.items[self.user2]}, self.s.groups["group2"])
def remove_entry(self, jid, *, timeout=None): """ Request removal of the roster entry identified by the given bare `jid`. If the entry currently has any subscription state, the server will send the corresponding unsubscribing presence stanzas. `timeout` is the maximum time in seconds to wait for a reply from the server. This may raise arbitrary :class:`.errors.XMPPError` exceptions if the server replies with an error and also any kind of connection error if the connection gets fatally terminated while waiting for a response. """ yield from self.client.stream.send( stanza.IQ( structs.IQType.SET, payload=roster_xso.Query(items=[ roster_xso.Item( jid=jid, subscription="remove" ) ]) ), timeout=timeout )
def test__validate_rejects_error_without_error(self): iq = stanza.IQ(structs.IQType.ERROR) iq.autoset_id() with self.assertRaisesRegex( ValueError, r"IQ with type='error' requires error payload"): iq._validate()
def test_handle_roster_push_accepts_push_from_bare_local_jid(self): self.cc.local_jid = structs.JID.fromstr("[email protected]/fnord") iq = stanza.IQ(type_=structs.IQType.SET) iq.from_ = structs.JID.fromstr("*****@*****.**") iq.payload = roster_xso.Query() run_coroutine(self.s.handle_roster_push(iq))
def test_validate_wraps_exceptions_from__validate(self): class FooException(Exception): pass iq = stanza.IQ(structs.IQType.GET) with self.assertRaisesRegex(stanza.StanzaError, r"invalid IQ stanza"): iq.validate()
def test_init(self): payload = TestPayload() s = stanza.IQ(from_=TEST_FROM, type_=structs.IQType.RESULT, payload=payload) self.assertEqual(TEST_FROM, s.from_) self.assertEqual(structs.IQType.RESULT, s.type_) self.assertIs(payload, s.payload)
def setUp(self): self.cc = make_connected_client() self.s = disco_service.Service(self.cc) self.cc.reset_mock() self.request_iq = stanza.IQ( "get", from_=structs.JID.fromstr("[email protected]/res1"), to=structs.JID.fromstr("[email protected]/res2")) self.request_iq.autoset_id() self.request_iq.payload = disco_xso.InfoQuery() self.request_items_iq = stanza.IQ( "get", from_=structs.JID.fromstr("[email protected]/res1"), to=structs.JID.fromstr("[email protected]/res2")) self.request_items_iq.autoset_id() self.request_items_iq.payload = disco_xso.ItemsQuery()
def send_and_decode_info_query(self, jid, node): request_iq = stanza.IQ(to=jid, type_="get") request_iq.payload = disco_xso.InfoQuery(node=node) response = yield from self.client.stream.send_iq_and_wait_for_reply( request_iq ) return response
def test_handle_roster_push_rejects_push_with_nonempty_from(self): iq = stanza.IQ(type_=structs.IQType.SET) iq.from_ = structs.JID.fromstr("*****@*****.**") with self.assertRaises(errors.XMPPAuthError) as ctx: run_coroutine(self.s.handle_roster_push(iq)) self.assertEqual((namespaces.stanzas, "forbidden"), ctx.exception.condition)
def set_entry(self, jid, *, name=_Sentinel, add_to_groups=frozenset(), remove_from_groups=frozenset(), timeout=None): """ Set properties of a roster entry or add a new roster entry. The roster entry is identified by its bare `jid`. If an entry already exists, all values default to those stored in the existing entry. For example, if no `name` is given, the current name of the entry is re-used, if any. If the entry does not exist, it will be created on the server side. The `remove_from_groups` and `add_to_groups` arguments have to be based on the locally cached state, as XMPP does not support sending diffs. `remove_from_groups` takes precedence over `add_to_groups`. `timeout` is the time in seconds to wait for a confirmation by the server. Note that the changes may not be visible immediately after his coroutine returns in the :attr:`items` and :attr:`groups` attributes. The :class:`Service` waits for the "official" roster push from the server for updating the data structures and firing events, to ensure that consistent state with other clients is achieved. This may raise arbitrary :class:`.errors.XMPPError` exceptions if the server replies with an error and also any kind of connection error if the connection gets fatally terminated while waiting for a response. """ existing = self.items.get(jid, Item(jid)) post_groups = (existing.groups | add_to_groups) - remove_from_groups post_name = existing.name if name is not _Sentinel: post_name = name item = roster_xso.Item( jid=jid, name=post_name, groups=[ roster_xso.Group(name=group_name) for group_name in post_groups ]) yield from self.client.stream.send( stanza.IQ( structs.IQType.SET, payload=roster_xso.Query(items=[ item ]) ), timeout=timeout )
def test_make_reply_enforces_request(self): s = stanza.IQ( from_=TEST_FROM, to=TEST_TO, id_="someid", type_="error") with self.assertRaises(ValueError): s.make_reply("error") s.type_ = "result" with self.assertRaises(ValueError): s.make_reply("error")
def query_items(self, jid, *, node=None, require_fresh=False, timeout=None): """ Send an items query to the given `jid`, querying for the items at the `node`. Return the :class:`~.xso.ItemsQuery` result. The arguments have the same semantics as with :meth:`query_info`, as does the caching and error handling. """ key = jid, node if not require_fresh: try: request = self._items_pending[key] except KeyError: pass else: try: return (yield from request) except asyncio.CancelledError: pass request_iq = stanza.IQ(to=jid, type_=structs.IQType.GET) request_iq.payload = disco_xso.ItemsQuery(node=node) request = asyncio. async ( self.client.stream.send_iq_and_wait_for_reply(request_iq)) self._items_pending[key] = request try: if timeout is not None: try: result = yield from asyncio.wait_for(request, timeout=timeout) except asyncio.TimeoutError: raise TimeoutError() else: result = yield from request except: if request.done(): try: pending = self._items_pending[key] except KeyError: pass else: if pending is request: del self._items_pending[key] raise return result
def test_make_reply(self): s = stanza.IQ(from_=TEST_FROM, to=TEST_TO, id_="someid", type_=structs.IQType.GET) r1 = s.make_reply(structs.IQType.ERROR) self.assertEqual(s.from_, r1.to) self.assertEqual(s.to, r1.from_) self.assertEqual(s.id_, r1.id_) self.assertEqual(structs.IQType.ERROR, r1.type_)
def test_handle_roster_push_rejects_push_with_nonempty_from(self): self.cc.local_jid = structs.JID.fromstr("*****@*****.**") iq = stanza.IQ(type_=structs.IQType.SET) iq.from_ = structs.JID.fromstr("*****@*****.**") with self.assertRaises(errors.XMPPAuthError) as ctx: run_coroutine(self.s.handle_roster_push(iq)) self.assertEqual(errors.ErrorCondition.FORBIDDEN, ctx.exception.condition)
def test_make_reply_enforces_request(self): s = stanza.IQ(from_=TEST_FROM, to=TEST_TO, id_="someid", type_=structs.IQType.ERROR) with self.assertRaisesRegex(ValueError, r"make_reply requires request IQ"): s.make_reply(unittest.mock.sentinel.type_) s.type_ = structs.IQType.RESULT with self.assertRaisesRegex(ValueError, r"make_reply requires request IQ"): s.make_reply(unittest.mock.sentinel.type_)
def test_handle_roster_push_rejects_push_from_full_local_jid(self): self.cc.local_jid = structs.JID.fromstr("[email protected]/fnord") iq = stanza.IQ(type_=structs.IQType.SET) iq.from_ = structs.JID.fromstr("[email protected]/fnord") iq.payload = roster_xso.Query() with self.assertRaises(errors.XMPPAuthError) as ctx: run_coroutine(self.s.handle_roster_push(iq)) self.assertEqual((namespaces.stanzas, "forbidden"), ctx.exception.condition)
def test_init_error(self): error = object() s = stanza.IQ( from_=TEST_FROM, type_="error", error=error) self.assertEqual( "error", s.type_) self.assertIs( error, s.error)
def test_handle_roster_push_removes_from_roster(self): request = roster_xso.Query(items=[ roster_xso.Item(jid=self.user1, subscription="remove"), ], ver="foobarbaz") iq = stanza.IQ(type_=structs.IQType.SET) iq.payload = request self.assertIsNone(run_coroutine(self.s.handle_roster_push(iq))) self.assertNotIn(self.user1, self.s.items) self.assertIn(self.user2, self.s.items) self.assertEqual("foobarbaz", self.s.version)
def test_make_error(self): e = stanza.Error(condition=(namespaces.stanzas, "bad-request")) s = stanza.IQ(from_=TEST_FROM, to=TEST_TO, id_="someid", type_=structs.IQType.GET) r = s.make_error(e) self.assertIsInstance(r, stanza.IQ) self.assertEqual(r.type_, structs.IQType.ERROR) self.assertEqual(TEST_FROM, r.to) self.assertEqual(TEST_TO, r.from_) self.assertEqual(s.id_, r.id_)
def test_make_error(self): e = stanza.Error(condition=errors.ErrorCondition.BAD_REQUEST) s = stanza.IQ(from_=TEST_FROM, to=TEST_TO, id_="someid", type_=structs.IQType.GET) r = s.make_error(e) self.assertIsInstance(r, stanza.IQ) self.assertEqual(r.type_, structs.IQType.ERROR) self.assertEqual(TEST_FROM, r.to) self.assertEqual(TEST_TO, r.from_) self.assertEqual(s.id_, r.id_)
def test_repr(self): s = stanza.IQ( from_=TEST_FROM, to=TEST_TO, id_="someid", type_="error") s.error = stanza.Error() self.assertEqual( "<iq from='*****@*****.**' to='*****@*****.**'" " id='someid' type='error'" " error=<undefined-condition type='cancel'>>", repr(s) ) s = stanza.IQ( from_=TEST_FROM, to=TEST_TO, id_="someid", type_="result") s.payload = TestPayload() self.assertEqual( "<iq from='*****@*****.**' to='*****@*****.**'" " id='someid' type='result'" " data=foobar>", repr(s) ) s = stanza.IQ( from_=TEST_FROM, to=TEST_TO, id_="someid", type_="result") self.assertEqual( "<iq from='*****@*****.**' to='*****@*****.**'" " id='someid' type='result'>", repr(s) )
def test_item_objects_do_not_change_during_push(self): old_item = self.s.items[self.user1] request = roster_xso.Query(items=[ roster_xso.Item(jid=self.user1, subscription="both"), ], ver="foobar") iq = stanza.IQ(type_=structs.IQType.SET) iq.payload = request self.assertIsNone(run_coroutine(self.s.handle_roster_push(iq))) self.assertIs(old_item, self.s.items[self.user1]) self.assertEqual("both", old_item.subscription)
def test_on_group_removed_for_removed_contact(self): request = roster_xso.Query(items=[ roster_xso.Item( jid=self.user2, subscription="remove", ), ], ver="foobar") iq = stanza.IQ(type_=structs.IQType.SET) iq.payload = request run_coroutine(self.s.handle_roster_push(iq)) self.listener.on_group_removed.assert_called_once_with("group2")
def test_init(self): payload = object() s = stanza.IQ( from_=TEST_FROM, type_="result", payload=payload) self.assertEqual( TEST_FROM, s.from_) self.assertEqual( "result", s.type_) self.assertIs( payload, s.payload)
def test_update_groups_on_update(self): request = roster_xso.Query(items=[ roster_xso.Item( jid=self.user1, groups=[roster_xso.Group(name="group4")], ) ]) run_coroutine( self.s.handle_roster_push(stanza.IQ("set", payload=request))) self.assertNotIn("group3", self.s.groups) self.assertSetEqual({self.s.items[self.user2]}, self.s.groups["group1"]) self.assertSetEqual({self.s.items[self.user2]}, self.s.groups["group2"]) self.assertSetEqual({self.s.items[self.user1]}, self.s.groups["group4"])
def test_do_not_lose_update_during_initial_roster(self): self.cc.mock_calls.clear() initial = roster_xso.Query(items=[ roster_xso.Item(jid=self.user2, name="some bar user", subscription="both") ], ver="foobar") push = stanza.IQ(type_=structs.IQType.SET, payload=roster_xso.Query(items=[ roster_xso.Item( jid=self.user1, name="some foo user", ), roster_xso.Item( jid=self.user2, subscription="remove", ) ], ver="foobar")) @asyncio.coroutine def send(iq, timeout=None): # this is brutal, but a sure way to provoke the race asyncio.ensure_future(self.s.handle_roster_push(push)) # give the roster push a chance to act # (we cannot yield from the handle_roster_push() here: in the fixed # version that would be a deadlock) yield from asyncio.sleep(0) return initial self.cc.send = unittest.mock.Mock() self.cc.send.side_effect = send initial_roster = asyncio.ensure_future( self.cc.before_stream_established()) run_coroutine(initial_roster) self.assertNotIn( self.user2, self.s.items, "initial roster processing lost a race against roster push")
def _request_initial_roster(self): iq = stanza.IQ(type_=structs.IQType.GET) iq.payload = roster_xso.Query() with (yield from self.__roster_lock): logger.debug("requesting initial roster") if self.client.stream_features.has_feature( roster_xso.RosterVersioningFeature): logger.debug("requesting incremental updates (old ver = %s)", self.version) iq.payload.ver = self.version response = yield from self.client.stream.send( iq, timeout=self.client.negotiation_timeout.total_seconds() ) if response is None: logger.debug("roster will be updated incrementally") self.on_initial_roster_received() return True self.version = response.ver logger.debug("roster update received (new ver = %s)", self.version) actual_jids = {item.jid for item in response.items} known_jids = set(self.items.keys()) removed_jids = known_jids - actual_jids logger.debug("jids dropped: %r", removed_jids) for removed_jid in removed_jids: old_item = self.items.pop(removed_jid) self._remove_from_groups(old_item, old_item.groups) self.on_entry_removed(old_item) logger.debug("jids updated: %r", actual_jids - removed_jids) for item in response.items: self._update_entry(item) self.on_initial_roster_received() return True