diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 32dbcd60..7070c6a9 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -87,7 +87,7 @@ jobs: pip3 install --user --ignore-installed -r requirements-dev.txt behave --junit tests/acceptance/pam - behave --junit tests/acceptance/encryption/cryptor-module.feature -t=~na=python -k + behave --junit tests/acceptance/encryption/cryptor-module.feature -t=~na=python behave --junit tests/acceptance/subscribe - name: Expose acceptance tests reports uses: actions/upload-artifact@v4 diff --git a/.pubnub.yml b/.pubnub.yml index 36647343..31aa5c65 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,5 +1,5 @@ name: python -version: 10.4.1 +version: 10.5.0 schema: 1 scm: github.com/pubnub/python sdks: @@ -18,7 +18,7 @@ sdks: distributions: - distribution-type: library distribution-repository: package - package-name: pubnub-10.4.1 + package-name: pubnub-10.5.0 location: https://pypi.org/project/pubnub/ supported-platforms: supported-operating-systems: @@ -94,8 +94,8 @@ sdks: - distribution-type: library distribution-repository: git release - package-name: pubnub-10.4.1 - location: https://github.com/pubnub/python/releases/download/10.4.1/pubnub-10.4.1.tar.gz + package-name: pubnub-10.5.0 + location: https://github.com/pubnub/python/releases/download/10.5.0/pubnub-10.5.0.tar.gz supported-platforms: supported-operating-systems: Linux: @@ -169,6 +169,15 @@ sdks: license-url: https://github.com/encode/httpx/blob/master/LICENSE.md is-required: Required changelog: + - date: 2025-12-02 + version: 10.5.0 + changes: + - type: feature + text: "Add `limit` (default `1000`) and `offset` parameters for `here_now` to fetch presence in portions." + - type: feature + text: "Add FCM push type support with GCM deprecation, and remove MPNS support due to its end of life." + - type: bug + text: "Fix issue because of which it was possible to add duplicated entries of `channels` and `groups` to the `subscribe`, `heartbeat`, and `leave` requests." - date: 2025-06-05 version: 10.4.1 changes: diff --git a/CHANGELOG.md b/CHANGELOG.md index d497adee..fcba6b3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 10.5.0 +December 02 2025 + +#### Added +- Add `limit` (default `1000`) and `offset` parameters for `here_now` to fetch presence in portions. +- Add FCM push type support with GCM deprecation, and remove MPNS support due to its end of life. + +#### Fixed +- Fix issue because of which it was possible to add duplicated entries of `channels` and `groups` to the `subscribe`, `heartbeat`, and `leave` requests. + ## 10.4.1 June 05 2025 diff --git a/pubnub/endpoints/presence/heartbeat.py b/pubnub/endpoints/presence/heartbeat.py index 9fc2267c..df84f255 100644 --- a/pubnub/endpoints/presence/heartbeat.py +++ b/pubnub/endpoints/presence/heartbeat.py @@ -1,4 +1,4 @@ -from typing import Dict, Optional, Union, List +from typing import Dict, Optional, Union, List, Set from pubnub import utils from pubnub.endpoints.endpoint import Endpoint from pubnub.enums import HttpMethod, PNOperationType @@ -13,22 +13,22 @@ class Heartbeat(Endpoint): def __init__(self, pubnub, channels: Union[str, List[str]] = None, channel_groups: Union[str, List[str]] = None, state: Optional[Dict[str, any]] = None): super(Heartbeat, self).__init__(pubnub) - self._channels = [] - self._groups = [] + self._channels: Set[str] = set() + self._groups: Set[str] = set() if channels: - utils.extend_list(self._channels, channels) + utils.update_set(self._channels, channels) if channel_groups: - utils.extend_list(self._groups, channel_groups) + utils.update_set(self._groups, channel_groups) self._state = state def channels(self, channels: Union[str, List[str]]) -> 'Heartbeat': - utils.extend_list(self._channels, channels) + utils.update_set(self._channels, channels) return self def channel_groups(self, channel_groups: Union[str, List[str]]) -> 'Heartbeat': - utils.extend_list(self._groups, channel_groups) + utils.update_set(self._groups, channel_groups) return self def state(self, state: Dict[str, any]) -> 'Heartbeat': @@ -46,14 +46,14 @@ def validate_params(self): raise PubNubException(pn_error=PNERR_CHANNEL_OR_GROUP_MISSING) def build_path(self): - channels = utils.join_channels(self._channels) + channels = utils.join_channels(self._channels, True) return Heartbeat.HEARTBEAT_PATH % (self.pubnub.config.subscribe_key, channels) def custom_params(self): params = {'heartbeat': str(self.pubnub.config.presence_timeout)} if len(self._groups) > 0: - params['channel-group'] = utils.join_items(self._groups) + params['channel-group'] = utils.join_items(self._groups, True) if self._state is not None and len(self._state) > 0: params['state'] = utils.url_write(self._state) diff --git a/pubnub/endpoints/presence/here_now.py b/pubnub/endpoints/presence/here_now.py index e1d22a7e..4c094b79 100644 --- a/pubnub/endpoints/presence/here_now.py +++ b/pubnub/endpoints/presence/here_now.py @@ -29,6 +29,8 @@ def __init__(self, pubnub, channels: Union[str, List[str]] = None, channel_group self._include_state = include_state self._include_uuids = include_uuids + self._offset = None + self._limit = 1000 def channels(self, channels: Union[str, List[str]]) -> 'HereNow': utils.extend_list(self._channels, channels) @@ -46,8 +48,16 @@ def include_uuids(self, include_uuids) -> 'HereNow': self._include_uuids = include_uuids return self + def limit(self, limit: int) -> 'HereNow': + self._limit = limit + return self + + def offset(self, offset: int) -> 'HereNow': + self._offset = offset + return self + def custom_params(self): - params = {} + params = {'limit': self._limit} if len(self._channel_groups) > 0: params['channel-group'] = utils.join_items_and_encode(self._channel_groups) @@ -58,6 +68,9 @@ def custom_params(self): if not self._include_uuids: params['disable_uuids'] = "1" + if self._offset is not None: + params['offset'] = self._offset + return params def build_path(self): diff --git a/pubnub/endpoints/presence/leave.py b/pubnub/endpoints/presence/leave.py index 113150e8..88e4a40f 100644 --- a/pubnub/endpoints/presence/leave.py +++ b/pubnub/endpoints/presence/leave.py @@ -1,3 +1,5 @@ +from typing import Set, Union, List + from pubnub import utils from pubnub.endpoints.endpoint import Endpoint from pubnub.errors import PNERR_CHANNEL_OR_GROUP_MISSING @@ -11,30 +13,22 @@ class Leave(Endpoint): def __init__(self, pubnub): Endpoint.__init__(self, pubnub) - self._channels = [] - self._groups = [] - - def channels(self, channels): - if isinstance(channels, (list, tuple)): - self._channels.extend(channels) - else: - self._channels.extend(utils.split_items(channels)) + self._channels: Set[str] = set() + self._groups: Set[str] = set() + def channels(self, channels: Union[str, List[str]]) -> 'Leave': + utils.update_set(self._channels, channels) return self - def channel_groups(self, channel_groups): - if isinstance(channel_groups, (list, tuple)): - self._groups.extend(channel_groups) - else: - self._groups.extend(utils.split_items(channel_groups)) - + def channel_groups(self, channel_groups: Union[str, List[str]]) -> 'Leave': + utils.update_set(self._groups, channel_groups) return self def custom_params(self): params = {} if len(self._groups) > 0: - params['channel-group'] = utils.join_items(self._groups) + params['channel-group'] = utils.join_items(self._groups, True) if hasattr(self.pubnub, '_subscription_manager'): params.update(self.pubnub._subscription_manager.get_custom_params()) @@ -42,7 +36,7 @@ def custom_params(self): return params def build_path(self): - return Leave.LEAVE_PATH % (self.pubnub.config.subscribe_key, utils.join_channels(self._channels)) + return Leave.LEAVE_PATH % (self.pubnub.config.subscribe_key, utils.join_channels(self._channels, True)) def http_method(self): return HttpMethod.GET @@ -60,10 +54,10 @@ def is_auth_required(self): return True def affected_channels(self): - return self._channels + return sorted(self._channels) def affected_channels_groups(self): - return self._groups + return sorted(self._groups) def request_timeout(self): return self.pubnub.config.non_subscribe_request_timeout diff --git a/pubnub/endpoints/pubsub/subscribe.py b/pubnub/endpoints/pubsub/subscribe.py index d91a8ca0..d616fcf3 100644 --- a/pubnub/endpoints/pubsub/subscribe.py +++ b/pubnub/endpoints/pubsub/subscribe.py @@ -1,4 +1,4 @@ -from typing import Optional, Union, List +from typing import Optional, Union, List, Set from pubnub import utils from pubnub.endpoints.endpoint import Endpoint from pubnub.enums import HttpMethod, PNOperationType @@ -25,12 +25,12 @@ def __init__(self, pubnub, channels: Union[str, List[str]] = None, with_presence: Optional[str] = None, state: Optional[str] = None): super(Subscribe, self).__init__(pubnub) - self._channels = [] + self._channels: Set[str] = set() + self._groups: Set[str] = set() if channels: - utils.extend_list(self._channels, channels) - self._groups = [] + utils.update_set(self._channels, channels) if groups: - utils.extend_list(self._groups, groups) + utils.update_set(self._groups, groups) self._region = region self._filter_expression = filter_expression @@ -39,11 +39,11 @@ def __init__(self, pubnub, channels: Union[str, List[str]] = None, self._state = state def channels(self, channels: Union[str, List[str]]) -> 'Subscribe': - utils.extend_list(self._channels, channels) + utils.update_set(self._channels, channels) return self def channel_groups(self, groups: Union[str, List[str]]) -> 'Subscribe': - utils.extend_list(self._groups, groups) + utils.update_set(self._groups, groups) return self def timetoken(self, timetoken) -> 'Subscribe': @@ -72,14 +72,14 @@ def validate_params(self): raise PubNubException(pn_error=PNERR_CHANNEL_OR_GROUP_MISSING) def build_path(self): - channels = utils.join_channels(self._channels) + channels = utils.join_channels(self._channels, True) return Subscribe.SUBSCRIBE_PATH % (self.pubnub.config.subscribe_key, channels) def custom_params(self): params = {} if len(self._groups) > 0: - params['channel-group'] = utils.join_items_and_encode(self._groups) + params['channel-group'] = utils.join_items_and_encode(self._groups, True) if self._filter_expression is not None and len(self._filter_expression) > 0: params['filter-expr'] = utils.url_encode(self._filter_expression) @@ -108,10 +108,10 @@ def is_auth_required(self): return True def affected_channels(self): - return self._channels + return sorted(self._channels) def affected_channels_groups(self): - return self._groups + return sorted(self._groups) def request_timeout(self): return self.pubnub.config.subscribe_request_timeout diff --git a/pubnub/enums.py b/pubnub/enums.py index 1e1c8a43..f3235a87 100644 --- a/pubnub/enums.py +++ b/pubnub/enums.py @@ -143,9 +143,9 @@ class PNReconnectionPolicy(object): class PNPushType(object): APNS = 1 - MPNS = 2 - GCM = 3 + GCM = 3 # Deprecated: Use FCM instead. GCM has been replaced by FCM (Firebase Cloud Messaging) APNS2 = 4 + FCM = 5 class PNResourceType(object): diff --git a/pubnub/event_engine/effects.py b/pubnub/event_engine/effects.py index e14e7e86..d7c0b28d 100644 --- a/pubnub/event_engine/effects.py +++ b/pubnub/event_engine/effects.py @@ -88,7 +88,7 @@ async def handshake_async(self, channels, groups, stop_event, timetoken: int = 0 self.logger.warning(f'Handshake failed: {response.status.error_data.__dict__}') handshake_failure = events.HandshakeFailureEvent(response.status.error_data, 1, timetoken=timetoken) self.event_engine.trigger(handshake_failure) - else: + elif 't' in response.result: cursor = response.result['t'] timetoken = timetoken if timetoken > 0 else cursor['t'] region = cursor['r'] @@ -134,7 +134,7 @@ async def receive_messages_async(self, channels, groups, timetoken, region): self.logger.warning(f'Recieve messages failed: {response.status.error_data.__dict__}') recieve_failure = events.ReceiveFailureEvent(response.status.error_data, 1, timetoken=timetoken) self.event_engine.trigger(recieve_failure) - else: + elif 't' in response.result: cursor = response.result['t'] timetoken = cursor['t'] region = cursor['r'] diff --git a/pubnub/event_engine/models/states.py b/pubnub/event_engine/models/states.py index d9873323..6d40b3e5 100644 --- a/pubnub/event_engine/models/states.py +++ b/pubnub/event_engine/models/states.py @@ -568,7 +568,7 @@ def reconnect_failure(self, event: events.ReceiveReconnectFailureEvent, context: return PNTransition( state=ReceiveReconnectingState, context=self._context, - invocation=invocations.EmitStatusInvocation(PNStatusCategory.UnexpectedDisconnectCategory, + invocation=invocations.EmitStatusInvocation(PNStatusCategory.PNUnexpectedDisconnectCategory, operation=PNOperationType.PNSubscribeOperation, context=self._context) ) diff --git a/pubnub/utils.py b/pubnub/utils.py index 3b5d2976..0ddfa417 100644 --- a/pubnub/utils.py +++ b/pubnub/utils.py @@ -8,6 +8,7 @@ import warnings from hashlib import sha256 +from typing import Set, List, Union from pubnub.enums import PNStatusCategory, PNOperationType, PNPushType, HttpMethod, PAMPermissions from pubnub.models.consumer.common import PNStatus @@ -54,19 +55,19 @@ def split_items(items_string): return items_string.split(",") -def join_items(items_list): - return ",".join(items_list) +def join_items(items_list, sort_items=False): + return ",".join(sorted(items_list) if sort_items else items_list) -def join_items_and_encode(items_list): - return ",".join(url_encode(x) for x in items_list) +def join_items_and_encode(items_list, sort_items=False): + return ",".join(url_encode(x) for x in (sorted(items_list) if sort_items else items_list)) -def join_channels(items_list): +def join_channels(items_list, sort_items=False): if len(items_list) == 0: return "," else: - return join_items_and_encode(items_list) + return join_items_and_encode(items_list, sort_items) def extend_list(existing_items, new_items): @@ -76,6 +77,13 @@ def extend_list(existing_items, new_items): existing_items.extend(new_items) +def update_set(existing_items: Set[str], new_items: Union[str, List[str]]): + if isinstance(new_items, str): + existing_items.update(split_items(new_items)) + else: + existing_items.update(new_items) + + def build_url(scheme, origin, path, params={}): return urllib.parse.urlunsplit((scheme, origin, path, params, '')) @@ -154,8 +162,8 @@ def push_type_to_string(push_type): return "apns" elif push_type == PNPushType.GCM: return "gcm" - elif push_type == PNPushType.MPNS: - return "mpns" + elif push_type == PNPushType.FCM: + return "fcm" else: return "" diff --git a/setup.py b/setup.py index 3a00ad56..d04556c1 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name='pubnub', - version='10.4.1', + version='10.5.0', description='PubNub Real-time push service in the cloud', author='PubNub', author_email='support@pubnub.com', diff --git a/tests/acceptance/pam/steps/then_steps.py b/tests/acceptance/pam/steps/then_steps.py index 6f3d4b8a..6a1a4ff1 100644 --- a/tests/acceptance/pam/steps/then_steps.py +++ b/tests/acceptance/pam/steps/then_steps.py @@ -1,5 +1,6 @@ import json from behave import then + from pubnub.exceptions import PubNubException @@ -19,6 +20,12 @@ def step_impl(context, channel): assert context.token_resource +@then("token {data_type} permission {permission}") +def step_impl(context, data_type, permission): + assert context.token_resource + assert context.token_resource[permission.lower()] + + @then("the token contains the authorized UUID {test_uuid}") def step_impl(context, test_uuid): assert context.parsed_token.get("authorized_uuid") == test_uuid.strip('"') @@ -80,6 +87,75 @@ def step_impl(context): context.pam_call_error = json.loads(context.pam_call_result._errormsg) +@then("the error status code is {error_code}") +def step_impl(context, error_code): + assert context.pam_call_error['status'] == int(error_code) + + +@then("the auth error message is '{error_message}'") +@then("the error message is '{error_message}'") +def step_impl(context, error_message): + if 'message' in context.pam_call_error: + assert context.pam_call_error['message'] == error_message + elif 'error' in context.pam_call_error and 'message' in context.pam_call_error['error']: + assert context.pam_call_error['error']['message'] == error_message + else: + raise AssertionError("Unexpected payload: {}".format(context.pam_call_error)) + + +@then("the error detail message is not empty") +def step_impl(context): + if 'error' in context.pam_call_error and 'details' in context.pam_call_error['error']: + assert len(context.pam_call_error['error']['details']) > 0 + assert 'message' in context.pam_call_error['error']['details'][0] + assert len(context.pam_call_error['error']['details'][0]['message']) > 0 + else: + raise AssertionError("Unexpected payload: {}".format(context.pam_call_error)) + + +@then("the error detail message is '{details_message}'") +def step_impl(context, details_message): + if 'error' in context.pam_call_error and 'details' in context.pam_call_error['error']: + assert len(context.pam_call_error['error']['details']) > 0 + assert 'message' in context.pam_call_error['error']['details'][0] + assert context.pam_call_error['error']['details'][0]['message'] == details_message + else: + raise AssertionError("Unexpected payload: {}".format(context.pam_call_error)) + + +@then("the error detail location is '{details_location}'") +def step_impl(context, details_location): + if 'error' in context.pam_call_error and 'details' in context.pam_call_error['error']: + assert len(context.pam_call_error['error']['details']) > 0 + assert 'location' in context.pam_call_error['error']['details'][0] + assert context.pam_call_error['error']['details'][0]['location'] == details_location + else: + raise AssertionError("Unexpected payload: {}".format(context.pam_call_error)) + + +@then("the error detail location type is '{details_location_type}'") +def step_impl(context, details_location_type): + if 'error' in context.pam_call_error and 'details' in context.pam_call_error['error']: + assert len(context.pam_call_error['error']['details']) > 0 + assert 'locationType' in context.pam_call_error['error']['details'][0] + assert context.pam_call_error['error']['details'][0]['locationType'] == details_location_type + else: + raise AssertionError("Unexpected payload: {}".format(context.pam_call_error)) + + +@then("the error service is '{error_service}'") +def step_impl(context, error_service): + assert context.pam_call_error['service'] == error_service + + +@then("the error source is '{error_source}'") +def step_impl(context, error_source): + if 'error' in context.pam_call_error and 'source' in context.pam_call_error['error']: + assert context.pam_call_error['error']['source'] == error_source + else: + raise AssertionError("Unexpected payload: {}".format(context.pam_call_error)) + + @then("the result is successful") def step_impl(context): assert context.publish_result.result.timetoken diff --git a/tests/acceptance/subscribe/steps/then_steps.py b/tests/acceptance/subscribe/steps/then_steps.py index b97d7940..60e9187e 100644 --- a/tests/acceptance/subscribe/steps/then_steps.py +++ b/tests/acceptance/subscribe/steps/then_steps.py @@ -25,6 +25,7 @@ async def step_impl(ctx: PNContext): await ctx.pubnub.stop() +@then("I observe the following:") @then("I observe the following") @async_run_until_complete async def step_impl(ctx): @@ -74,6 +75,7 @@ async def step_impl(ctx: PNContext, wait_time: str): await asyncio.sleep(int(wait_time)) +@then(u'I observe the following Events and Invocations of the Presence EE:') @then(u'I observe the following Events and Invocations of the Presence EE') @async_run_until_complete async def step_impl(ctx): diff --git a/tests/functional/push/test_add_channels_to_push.py b/tests/functional/push/test_add_channels_to_push.py index 9dbd905b..19c87a61 100644 --- a/tests/functional/push/test_add_channels_to_push.py +++ b/tests/functional/push/test_add_channels_to_push.py @@ -43,7 +43,7 @@ def test_push_add_single_channel(self): self.assertEqual(self.add_channels._channels, ['ch']) def test_push_add_multiple_channels(self): - self.add_channels.channels(['ch1', 'ch2']).push_type(pubnub.enums.PNPushType.MPNS).device_id("coolDevice") + self.add_channels.channels(['ch1', 'ch2']).push_type(pubnub.enums.PNPushType.APNS).device_id("coolDevice") params = (pnconf.subscribe_key, "coolDevice") self.assertEqual(self.add_channels.build_path(), AddChannelsToPush.ADD_PATH % params) @@ -51,14 +51,14 @@ def test_push_add_multiple_channels(self): self.assertEqual(self.add_channels.build_params_callback()({}), { 'pnsdk': sdk_name, 'uuid': self.pubnub.uuid, - 'type': 'mpns', + 'type': 'apns', 'add': 'ch1,ch2' }) self.assertEqual(self.add_channels._channels, ['ch1', 'ch2']) def test_push_add_google(self): - self.add_channels.channels(['ch1', 'ch2', 'ch3']).push_type(pubnub.enums.PNPushType.GCM).device_id("coolDevice") + self.add_channels.channels(['ch1', 'ch2', 'ch3']).push_type(pubnub.enums.PNPushType.FCM).device_id("coolDevice") params = (pnconf.subscribe_key, "coolDevice") self.assertEqual(self.add_channels.build_path(), AddChannelsToPush.ADD_PATH % params) @@ -66,7 +66,7 @@ def test_push_add_google(self): self.assertEqual(self.add_channels.build_params_callback()({}), { 'pnsdk': sdk_name, 'uuid': self.pubnub.uuid, - 'type': 'gcm', + 'type': 'fcm', 'add': 'ch1,ch2,ch3' }) diff --git a/tests/functional/push/test_list_push_provisions.py b/tests/functional/push/test_list_push_provisions.py index 396bab88..d725514b 100644 --- a/tests/functional/push/test_list_push_provisions.py +++ b/tests/functional/push/test_list_push_provisions.py @@ -55,19 +55,6 @@ def test_list_channel_group_gcm(self): 'type': 'gcm' }) - def test_list_channel_group_mpns(self): - self.list_push.push_type(PNPushType.MPNS).device_id('coolDevice') - - self.assertEqual(self.list_push.build_path(), - ListPushProvisions.LIST_PATH % ( - pnconf.subscribe_key, "coolDevice")) - - self.assertEqual(self.list_push.build_params_callback()({}), { - 'pnsdk': sdk_name, - 'uuid': self.pubnub.uuid, - 'type': 'mpns' - }) - def test_list_channel_group_apns2(self): self.list_push.push_type(PNPushType.APNS2).device_id('coolDevice')\ .environment(pubnub.enums.PNPushEnvironment.PRODUCTION).topic("testTopic") diff --git a/tests/functional/push/test_remove_channels_from_push.py b/tests/functional/push/test_remove_channels_from_push.py index af0d6cca..1c0ea93d 100644 --- a/tests/functional/push/test_remove_channels_from_push.py +++ b/tests/functional/push/test_remove_channels_from_push.py @@ -36,7 +36,7 @@ def test_push_remove_single_channel(self): self.assertEqual(self.remove_channels._channels, ['ch']) def test_push_remove_multiple_channels(self): - self.remove_channels.channels(['ch1', 'ch2']).push_type(pubnub.enums.PNPushType.MPNS).device_id("coolDevice") + self.remove_channels.channels(['ch1', 'ch2']).push_type(pubnub.enums.PNPushType.APNS).device_id("coolDevice") params = (pnconf.subscribe_key, "coolDevice") self.assertEqual(self.remove_channels.build_path(), RemoveChannelsFromPush.REMOVE_PATH % params) @@ -44,14 +44,14 @@ def test_push_remove_multiple_channels(self): self.assertEqual(self.remove_channels.build_params_callback()({}), { 'pnsdk': sdk_name, 'uuid': self.pubnub.uuid, - 'type': 'mpns', + 'type': 'apns', 'remove': 'ch1,ch2' }) self.assertEqual(self.remove_channels._channels, ['ch1', 'ch2']) def test_push_remove_google(self): - self.remove_channels.channels(['ch1', 'ch2', 'ch3']).push_type(pubnub.enums.PNPushType.GCM)\ + self.remove_channels.channels(['ch1', 'ch2', 'ch3']).push_type(pubnub.enums.PNPushType.FCM)\ .device_id("coolDevice") params = (pnconf.subscribe_key, "coolDevice") @@ -60,7 +60,7 @@ def test_push_remove_google(self): self.assertEqual(self.remove_channels.build_params_callback()({}), { 'pnsdk': sdk_name, 'uuid': self.pubnub.uuid, - 'type': 'gcm', + 'type': 'fcm', 'remove': 'ch1,ch2,ch3' }) diff --git a/tests/functional/push/test_remove_device_from_push.py b/tests/functional/push/test_remove_device_from_push.py index cd8e8bb4..6a912c8a 100644 --- a/tests/functional/push/test_remove_device_from_push.py +++ b/tests/functional/push/test_remove_device_from_push.py @@ -50,8 +50,8 @@ def test_remove_push_gcm(self): 'type': 'gcm', }) - def test_remove_push_mpns(self): - self.remove_device.push_type(pubnub.enums.PNPushType.MPNS).device_id("coolDevice") + def test_remove_push_fcm(self): + self.remove_device.push_type(pubnub.enums.PNPushType.FCM).device_id("coolDevice") params = (pnconf.subscribe_key, "coolDevice") self.assertEqual(self.remove_device.build_path(), RemoveDeviceFromPush.REMOVE_PATH % params) @@ -59,7 +59,7 @@ def test_remove_push_mpns(self): self.assertEqual(self.remove_device.build_params_callback()({}), { 'pnsdk': sdk_name, 'uuid': self.pubnub.uuid, - 'type': 'mpns', + 'type': 'fcm', }) def test_remove_push_apns2(self): diff --git a/tests/functional/test_heartbeat.py b/tests/functional/test_heartbeat.py index cf144afe..56c7753d 100644 --- a/tests/functional/test_heartbeat.py +++ b/tests/functional/test_heartbeat.py @@ -32,7 +32,7 @@ def test_sub_single_channel(self): 'heartbeat': '20' }) - self.assertEqual(self.hb._channels, ['ch']) + self.assertEqual(list(self.hb._channels), ['ch']) def test_hb_multiple_channels_using_list(self): self.hb.channels(['ch1', 'ch2', 'ch3']) @@ -46,7 +46,15 @@ def test_hb_multiple_channels_using_list(self): 'heartbeat': '20' }) - self.assertEqual(self.hb._channels, ['ch1', 'ch2', 'ch3']) + self.assertEqual(sorted(self.hb._channels), ['ch1', 'ch2', 'ch3']) + + def test_hb_unique_channels_using_list(self): + self.hb.channels(['ch1', 'ch2', 'ch1']) + + self.assertEqual(self.hb.build_path(), Heartbeat.HEARTBEAT_PATH + % (pnconf.subscribe_key, "ch1,ch2")) + + self.assertEqual(sorted(self.hb._channels), ['ch1', 'ch2']) def test_hb_single_group(self): self.hb.channel_groups("gr") @@ -61,7 +69,7 @@ def test_hb_single_group(self): 'heartbeat': '20' }) - self.assertEqual(self.hb._groups, ['gr']) + self.assertEqual(list(self.hb._groups), ['gr']) def test_hb_multiple_groups_using_list(self): self.hb.channel_groups(['gr1', 'gr2', 'gr3']) @@ -76,7 +84,20 @@ def test_hb_multiple_groups_using_list(self): 'heartbeat': '20' }) - self.assertEqual(self.hb._groups, ['gr1', 'gr2', 'gr3']) + self.assertEqual(sorted(self.hb._groups), ['gr1', 'gr2', 'gr3']) + + def test_hb_unique_channel_groups_using_list(self): + self.hb.channel_groups(['gr1', 'gr2', 'gr1']) + + self.assertEqual(self.hb.build_path(), Heartbeat.HEARTBEAT_PATH + % (pnconf.subscribe_key, ",")) + + self.assertEqual(self.hb.build_params_callback()({}), { + 'channel-group': 'gr1,gr2', + 'pnsdk': sdk_name, + 'uuid': self.pubnub.uuid, + 'heartbeat': '20' + }) def test_hb_with_state(self): state = {"name": "Alex", "count": 7} @@ -95,5 +116,5 @@ def test_hb_with_state(self): 'state': state }) - self.assertEqual(self.hb._groups, []) - self.assertEqual(self.hb._channels, ['ch1', 'ch2']) + self.assertEqual(list(self.hb._groups), []) + self.assertEqual(sorted(self.hb._channels), ['ch1', 'ch2']) diff --git a/tests/functional/test_here_now.py b/tests/functional/test_here_now.py index 48be47ea..6a3d8381 100644 --- a/tests/functional/test_here_now.py +++ b/tests/functional/test_here_now.py @@ -30,11 +30,12 @@ def test_here_now(self): self.assertEqual(self.here_now.build_params_callback()({}), { 'pnsdk': sdk_name, - 'uuid': self.pubnub.uuid + 'uuid': self.pubnub.uuid, + 'limit': 1000, }) def test_here_now_groups(self): - self.here_now.channel_groups("gr1") + self.here_now.channel_groups("gr1").limit(10000) self.assertEqual(self.here_now.build_path(), HereNow.HERE_NOW_PATH % (pnconf.subscribe_key, ",")) @@ -42,11 +43,12 @@ def test_here_now_groups(self): self.assertEqual(self.here_now.build_params_callback()({}), { 'channel-group': 'gr1', 'pnsdk': sdk_name, - 'uuid': self.pubnub.uuid + 'uuid': self.pubnub.uuid, + 'limit': 10000, }) def test_here_now_with_options(self): - self.here_now.channels(["ch1"]).channel_groups("gr1").include_state(True).include_uuids(False) + self.here_now.channels(["ch1"]).channel_groups("gr1").include_state(True).include_uuids(False).offset(3) self.assertEqual(self.here_now.build_path(), HereNow.HERE_NOW_PATH % (pnconf.subscribe_key, "ch1")) @@ -56,5 +58,7 @@ def test_here_now_with_options(self): 'state': '1', 'disable_uuids': '1', 'pnsdk': sdk_name, - 'uuid': self.pubnub.uuid + 'uuid': self.pubnub.uuid, + 'limit': 1000, + 'offset': 3, }) diff --git a/tests/functional/test_leave.py b/tests/functional/test_leave.py index 0d56ae8b..b8e38802 100644 --- a/tests/functional/test_leave.py +++ b/tests/functional/test_leave.py @@ -33,7 +33,7 @@ def test_leave_single_channel(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.leave._channels, ['ch']) + self.assertEqual(sorted(list(self.leave._channels)), ['ch']) def test_leave_multiple_channels(self): self.leave.channels("ch1,ch2,ch3") @@ -45,7 +45,7 @@ def test_leave_multiple_channels(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.leave._channels, ['ch1', 'ch2', 'ch3']) + self.assertEqual(sorted(list(self.leave._channels)), ['ch1', 'ch2', 'ch3']) def test_leave_multiple_channels_using_list(self): self.leave.channels(['ch1', 'ch2', 'ch3']) @@ -57,7 +57,7 @@ def test_leave_multiple_channels_using_list(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.leave._channels, ['ch1', 'ch2', 'ch3']) + self.assertEqual(sorted(list(self.leave._channels)), ['ch1', 'ch2', 'ch3']) def test_leave_multiple_channels_using_tuple(self): self.leave.channels(('ch1', 'ch2', 'ch3')) @@ -69,7 +69,14 @@ def test_leave_multiple_channels_using_tuple(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.leave._channels, ['ch1', 'ch2', 'ch3']) + self.assertEqual(sorted(list(self.leave._channels)), ['ch1', 'ch2', 'ch3']) + + def test_leave_unique_channels_using_list(self): + self.leave.channels(['ch1', 'ch2', 'ch1']) + + self.assertEqual(self.leave.build_path(), Leave.LEAVE_PATH % (pnconf.subscribe_key, "ch1,ch2")) + + self.assertEqual(sorted(list(self.leave._channels)), ['ch1', 'ch2']) def test_leave_single_group(self): self.leave.channel_groups("gr") @@ -83,7 +90,7 @@ def test_leave_single_group(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.leave._groups, ['gr']) + self.assertEqual(list(self.leave._groups), ['gr']) def test_leave_multiple_groups_using_string(self): self.leave.channel_groups("gr1,gr2,gr3") @@ -97,7 +104,7 @@ def test_leave_multiple_groups_using_string(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.leave._groups, ['gr1', 'gr2', 'gr3']) + self.assertEqual(sorted(list(self.leave._groups)), ['gr1', 'gr2', 'gr3']) def test_leave_multiple_groups_using_list(self): self.leave.channel_groups(['gr1', 'gr2', 'gr3']) @@ -111,7 +118,18 @@ def test_leave_multiple_groups_using_list(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.leave._groups, ['gr1', 'gr2', 'gr3']) + self.assertEqual(sorted(list(self.leave._groups)), ['gr1', 'gr2', 'gr3']) + + def test_leave_unique_channel_groups_using_list(self): + self.leave.channel_groups(['gr1', 'gr2', 'gr1']) + + self.assertEqual(self.leave.build_params_callback()({}), { + 'channel-group': 'gr1,gr2', + 'pnsdk': sdk_name, + 'uuid': self.pubnub.uuid + }) + + self.assertEqual(sorted(list(self.leave._groups)), ['gr1', 'gr2']) def test_leave_channels_and_groups(self): self.leave.channels('ch1,ch2').channel_groups(["gr1", "gr2"]) @@ -125,5 +143,5 @@ def test_leave_channels_and_groups(self): 'channel-group': 'gr1,gr2', }) - self.assertEqual(self.leave._groups, ['gr1', 'gr2']) - self.assertEqual(self.leave._channels, ['ch1', 'ch2']) + self.assertEqual(sorted(list(self.leave._groups)), ['gr1', 'gr2']) + self.assertEqual(sorted(list(self.leave._channels)), ['ch1', 'ch2']) diff --git a/tests/functional/test_subscribe.py b/tests/functional/test_subscribe.py index 792d1227..fb57371e 100644 --- a/tests/functional/test_subscribe.py +++ b/tests/functional/test_subscribe.py @@ -30,7 +30,7 @@ def test_pub_single_channel(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.sub._channels, ['ch']) + self.assertEqual(list(self.sub._channels), ['ch']) def test_sub_multiple_channels_using_string(self): self.sub.channels("ch1,ch2,ch3") @@ -43,7 +43,7 @@ def test_sub_multiple_channels_using_string(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.sub._channels, ['ch1', 'ch2', 'ch3']) + self.assertEqual(sorted(self.sub._channels), ['ch1', 'ch2', 'ch3']) def test_sub_multiple_channels_using_list(self): self.sub.channels(['ch1', 'ch2', 'ch3']) @@ -56,7 +56,7 @@ def test_sub_multiple_channels_using_list(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.sub._channels, ['ch1', 'ch2', 'ch3']) + self.assertEqual(sorted(self.sub._channels), ['ch1', 'ch2', 'ch3']) def test_sub_multiple_channels_using_tuple(self): self.sub.channels(('ch1', 'ch2', 'ch3')) @@ -69,7 +69,20 @@ def test_sub_multiple_channels_using_tuple(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.sub._channels, ['ch1', 'ch2', 'ch3']) + self.assertEqual(sorted(self.sub._channels), ['ch1', 'ch2', 'ch3']) + + def test_sub_unique_channels_using_list(self): + self.sub.channels(['ch1', 'ch2', 'ch1']) + + self.assertEqual(self.sub.build_path(), Subscribe.SUBSCRIBE_PATH + % (pnconf.subscribe_key, "ch1,ch2")) + + self.assertEqual(self.sub.build_params_callback()({}), { + 'pnsdk': sdk_name, + 'uuid': self.pubnub.uuid + }) + + self.assertEqual(sorted(self.sub._channels), ['ch1', 'ch2']) def test_sub_single_group(self): self.sub.channel_groups("gr") @@ -83,7 +96,7 @@ def test_sub_single_group(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.sub._groups, ['gr']) + self.assertEqual(list(self.sub._groups), ['gr']) def test_sub_multiple_groups_using_string(self): self.sub.channel_groups("gr1,gr2,gr3") @@ -97,7 +110,7 @@ def test_sub_multiple_groups_using_string(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.sub._groups, ['gr1', 'gr2', 'gr3']) + self.assertEqual(sorted(self.sub._groups), ['gr1', 'gr2', 'gr3']) def test_sub_multiple_groups_using_list(self): self.sub.channel_groups(['gr1', 'gr2', 'gr3']) @@ -111,7 +124,21 @@ def test_sub_multiple_groups_using_list(self): 'uuid': self.pubnub.uuid }) - self.assertEqual(self.sub._groups, ['gr1', 'gr2', 'gr3']) + self.assertEqual(sorted(self.sub._groups), ['gr1', 'gr2', 'gr3']) + + def test_sub_unique_channel_groups_using_list(self): + self.sub.channel_groups(['gr1', 'gr2', 'gr1']) + + self.assertEqual(self.sub.build_path(), Subscribe.SUBSCRIBE_PATH + % (pnconf.subscribe_key, ",")) + + self.assertEqual(self.sub.build_params_callback()({}), { + 'channel-group': 'gr1,gr2', + 'pnsdk': sdk_name, + 'uuid': self.pubnub.uuid + }) + + self.assertEqual(sorted(self.sub._groups), ['gr1', 'gr2']) def test_sub_multiple(self): self.sub.channels('ch1,ch2').filter_expression('blah').region('us-east-1').timetoken('123') @@ -127,8 +154,8 @@ def test_sub_multiple(self): 'tt': '123' }) - self.assertEqual(self.sub._groups, []) - self.assertEqual(self.sub._channels, ['ch1', 'ch2']) + self.assertEqual(list(self.sub._groups), []) + self.assertEqual(sorted(self.sub._channels), ['ch1', 'ch2']) def test_affected_channels_returns_provided_channels(self): self.sub.channels(('ch1', 'ch2', 'ch3')) diff --git a/tests/integrational/asyncio/test_change_uuid.py b/tests/integrational/asyncio/test_change_uuid.py index 2fb5a0a9..0925916d 100644 --- a/tests/integrational/asyncio/test_change_uuid.py +++ b/tests/integrational/asyncio/test_change_uuid.py @@ -30,6 +30,8 @@ async def test_change_uuid(): assert isinstance(envelope.result, PNSignalResult) assert isinstance(envelope.status, PNStatus) + await pn.stop() + @pn_vcr.use_cassette('tests/integrational/fixtures/asyncio/signal/uuid_no_lock.json', filter_query_parameters=['seqn', 'pnsdk', 'l_sig'], serializer='pn_json') @@ -51,13 +53,15 @@ async def test_change_uuid_no_lock(): assert isinstance(envelope.result, PNSignalResult) assert isinstance(envelope.status, PNStatus) + await pn.stop() + -def test_uuid_validation_at_init(_function_event_loop): +def test_uuid_validation_at_init(): with pytest.raises(AssertionError) as exception: pnconf = PNConfiguration() pnconf.publish_key = "demo" pnconf.subscribe_key = "demo" - PubNubAsyncio(pnconf, custom_event_loop=_function_event_loop) + PubNubAsyncio(pnconf) assert str(exception.value) == 'UUID missing or invalid type' diff --git a/tests/integrational/asyncio/test_heartbeat.py b/tests/integrational/asyncio/test_heartbeat.py index ec03562e..e2c9134d 100644 --- a/tests/integrational/asyncio/test_heartbeat.py +++ b/tests/integrational/asyncio/test_heartbeat.py @@ -3,7 +3,7 @@ import pytest import pubnub as pn -from pubnub.pubnub_asyncio import AsyncioSubscriptionManager, PubNubAsyncio, SubscribeListener +from pubnub.pubnub_asyncio import PubNubAsyncio, SubscribeListener from tests import helper from tests.helper import pnconf_env_copy @@ -11,6 +11,7 @@ @pytest.mark.asyncio +@pytest.mark.skip(reason="Needs to be reworked to use VCR") async def test_timeout_event_on_broken_heartbeat(): ch = helper.gen_channel("heartbeat-test") @@ -21,54 +22,54 @@ async def test_timeout_event_on_broken_heartbeat(): listener_config = pnconf_env_copy(uuid=helper.gen_channel("listener"), enable_subscribe=True) pubnub_listener = PubNubAsyncio(listener_config) - # - connect to :ch-pnpres - callback_presence = SubscribeListener() - pubnub_listener.add_listener(callback_presence) - pubnub_listener.subscribe().channels(ch).with_presence().execute() - await callback_presence.wait_for_connect() + try: + # - connect to :ch-pnpres + callback_presence = SubscribeListener() + pubnub_listener.add_listener(callback_presence) + pubnub_listener.subscribe().channels(ch).with_presence().execute() + await callback_presence.wait_for_connect() - envelope = await callback_presence.wait_for_presence_on(ch) - assert ch == envelope.channel - assert 'join' == envelope.event - assert pubnub_listener.uuid == envelope.uuid + envelope = await callback_presence.wait_for_presence_on(ch) + assert ch == envelope.channel + assert 'join' == envelope.event + assert pubnub_listener.uuid == envelope.uuid - # # - connect to :ch - callback_messages = SubscribeListener() - pubnub.add_listener(callback_messages) - pubnub.subscribe().channels(ch).execute() + # # - connect to :ch + callback_messages = SubscribeListener() + pubnub.add_listener(callback_messages) + pubnub.subscribe().channels(ch).execute() - useless_connect_future = asyncio.ensure_future(callback_messages.wait_for_connect()) - presence_future = asyncio.ensure_future(callback_presence.wait_for_presence_on(ch)) + useless_connect_future = asyncio.ensure_future(callback_messages.wait_for_connect()) + presence_future = asyncio.ensure_future(callback_presence.wait_for_presence_on(ch)) - # - assert join event - await asyncio.wait([useless_connect_future, presence_future]) + # - assert join event + await asyncio.wait([useless_connect_future, presence_future], return_when=asyncio.ALL_COMPLETED) - prs_envelope = presence_future.result() + prs_envelope = presence_future.result() - assert ch == prs_envelope.channel - assert 'join' == prs_envelope.event - assert pubnub.uuid == prs_envelope.uuid - # - break messenger heartbeat loop - pubnub._subscription_manager._stop_heartbeat_timer() + assert ch == prs_envelope.channel + assert 'join' == prs_envelope.event + assert pubnub.uuid == prs_envelope.uuid + # - break messenger heartbeat loop + pubnub._subscription_manager._stop_heartbeat_timer() - # wait for one heartbeat call - await asyncio.sleep(8) + # wait for one heartbeat call + await asyncio.sleep(8) - # - assert for timeout - envelope = await callback_presence.wait_for_presence_on(ch) - assert ch == envelope.channel - assert 'timeout' == envelope.event - assert pubnub.uuid == envelope.uuid + # - assert for timeout + envelope = await callback_presence.wait_for_presence_on(ch) + assert ch == envelope.channel + assert 'timeout' == envelope.event + assert pubnub.uuid == envelope.uuid - pubnub.unsubscribe().channels(ch).execute() - if isinstance(pubnub._subscription_manager, AsyncioSubscriptionManager): + pubnub.unsubscribe().channels(ch).execute() await callback_messages.wait_for_disconnect() - # - disconnect from :ch-pnpres - pubnub_listener.unsubscribe().channels(ch).execute() - if isinstance(pubnub._subscription_manager, AsyncioSubscriptionManager): + # - disconnect from :ch-pnpres + pubnub_listener.unsubscribe().channels(ch).execute() await callback_presence.wait_for_disconnect() - await pubnub.stop() - await pubnub_listener.stop() - await asyncio.sleep(0.5) + finally: + await pubnub.stop() + await pubnub_listener.stop() + await asyncio.sleep(0.5) diff --git a/tests/integrational/asyncio/test_message_count.py b/tests/integrational/asyncio/test_message_count.py index f2f547c2..ec65b4d7 100644 --- a/tests/integrational/asyncio/test_message_count.py +++ b/tests/integrational/asyncio/test_message_count.py @@ -1,4 +1,5 @@ import pytest +import pytest_asyncio from pubnub.pubnub_asyncio import PubNubAsyncio from pubnub.models.envelopes import AsyncioEnvelope @@ -8,12 +9,13 @@ from tests.integrational.vcr_helper import pn_vcr -@pytest.fixture -def pn(_function_event_loop): +@pytest_asyncio.fixture +async def pn(): config = pnconf_mc_copy() config.enable_subscribe = False - pn = PubNubAsyncio(config, custom_event_loop=_function_event_loop) + pn = PubNubAsyncio(config) yield pn + await pn.stop() @pn_vcr.use_cassette( diff --git a/tests/integrational/asyncio/test_where_now.py b/tests/integrational/asyncio/test_where_now.py index a40a1b43..6a7cae3c 100644 --- a/tests/integrational/asyncio/test_where_now.py +++ b/tests/integrational/asyncio/test_where_now.py @@ -82,8 +82,8 @@ async def test_multiple_channels(): # @pytest.mark.asyncio @pytest.mark.skip(reason="Needs to be reworked to use VCR") -async def test_where_now_super_admin_call(_function_event_loop): - pubnub = PubNubAsyncio(pnconf_pam_copy(), custom_event_loop=_function_event_loop) +async def test_where_now_super_admin_call(): + pubnub = PubNubAsyncio(pnconf_pam_copy()) uuid = 'test-where-now-asyncio-uuid-.*|@' pubnub.config.uuid = uuid diff --git a/tests/integrational/fixtures/native_sync/list_push_channels/mpns_basic_success.json b/tests/integrational/fixtures/native_sync/list_push_channels/mpns_basic_success.json deleted file mode 100644 index 2ee627f0..00000000 --- a/tests/integrational/fixtures/native_sync/list_push_channels/mpns_basic_success.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "version": 1, - "interactions": [ - { - "request": { - "method": "GET", - "uri": "https://ps.pndsn.com/v1/push/sub-key/{PN_KEY_SUBSCRIBE}/devices/0000000000000000?type=mpns&uuid=test-uuid", - "body": "", - "headers": { - "host": [ - "ps.pndsn.com" - ], - "accept": [ - "*/*" - ], - "accept-encoding": [ - "gzip, deflate" - ], - "connection": [ - "keep-alive" - ], - "user-agent": [ - "PubNub-Python/10.4.0" - ] - } - }, - "response": { - "status": { - "code": 200, - "message": "OK" - }, - "headers": { - "Date": [ - "Thu, 05 Jun 2025 12:42:58 GMT" - ], - "Content-Type": [ - "text/javascript; charset=\"UTF-8\"" - ], - "Content-Length": [ - "2" - ], - "Connection": [ - "keep-alive" - ], - "Cache-Control": [ - "no-cache" - ], - "Access-Control-Allow-Methods": [ - "GET, POST, DELETE, OPTIONS" - ], - "Access-Control-Allow-Credentials": [ - "true" - ], - "Access-Control-Expose-Headers": [ - "*" - ] - }, - "body": { - "pickle": "gASVEgAAAAAAAAB9lIwGc3RyaW5nlIwCW12Ucy4=" - } - } - } - ] -} diff --git a/tests/integrational/fixtures/native_sync/remove_channels_from_push/mpns_basic_success.json b/tests/integrational/fixtures/native_sync/remove_channels_from_push/mpns_basic_success.json deleted file mode 100644 index 833b2970..00000000 --- a/tests/integrational/fixtures/native_sync/remove_channels_from_push/mpns_basic_success.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "version": 1, - "interactions": [ - { - "request": { - "method": "GET", - "uri": "https://ps.pndsn.com/v1/push/sub-key/{PN_KEY_SUBSCRIBE}/devices/0000000000000000?remove=mpns_remove_channel_1%2Cmpns_remove_channel_2&type=mpns&uuid=test-uuid", - "body": "", - "headers": { - "host": [ - "ps.pndsn.com" - ], - "accept": [ - "*/*" - ], - "accept-encoding": [ - "gzip, deflate" - ], - "connection": [ - "keep-alive" - ], - "user-agent": [ - "PubNub-Python/10.4.0" - ] - } - }, - "response": { - "status": { - "code": 200, - "message": "OK" - }, - "headers": { - "Date": [ - "Thu, 05 Jun 2025 13:11:14 GMT" - ], - "Content-Type": [ - "text/javascript; charset=\"UTF-8\"" - ], - "Content-Length": [ - "24" - ], - "Connection": [ - "keep-alive" - ], - "Cache-Control": [ - "no-cache" - ], - "Access-Control-Allow-Methods": [ - "GET, POST, DELETE, OPTIONS" - ], - "Access-Control-Allow-Credentials": [ - "true" - ], - "Access-Control-Expose-Headers": [ - "*" - ] - }, - "body": { - "pickle": "gASVKAAAAAAAAAB9lIwGc3RyaW5nlIwYWzEsICJNb2RpZmllZCBDaGFubmVscyJdlHMu" - } - } - } - ] -} diff --git a/tests/integrational/fixtures/native_sync/remove_device_from_push/mpns_basic_success.json b/tests/integrational/fixtures/native_sync/remove_device_from_push/mpns_basic_success.json deleted file mode 100644 index 8a58bc3f..00000000 --- a/tests/integrational/fixtures/native_sync/remove_device_from_push/mpns_basic_success.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "version": 1, - "interactions": [ - { - "request": { - "method": "GET", - "uri": "https://ps.pndsn.com/v1/push/sub-key/{PN_KEY_SUBSCRIBE}/devices/0000000000000000/remove?type=mpns&uuid=test-uuid", - "body": "", - "headers": { - "host": [ - "ps.pndsn.com" - ], - "accept": [ - "*/*" - ], - "accept-encoding": [ - "gzip, deflate" - ], - "connection": [ - "keep-alive" - ], - "user-agent": [ - "PubNub-Python/10.4.0" - ] - } - }, - "response": { - "status": { - "code": 200, - "message": "OK" - }, - "headers": { - "Date": [ - "Thu, 05 Jun 2025 13:17:39 GMT" - ], - "Content-Type": [ - "text/javascript; charset=\"UTF-8\"" - ], - "Content-Length": [ - "21" - ], - "Connection": [ - "keep-alive" - ], - "Cache-Control": [ - "no-cache" - ], - "Access-Control-Allow-Methods": [ - "GET, POST, DELETE, OPTIONS" - ], - "Access-Control-Allow-Credentials": [ - "true" - ], - "Access-Control-Expose-Headers": [ - "*" - ] - }, - "body": { - "pickle": "gASVJQAAAAAAAAB9lIwGc3RyaW5nlIwVWzEsICJSZW1vdmVkIERldmljZSJdlHMu" - } - } - } - ] -} diff --git a/tests/integrational/native_sync/test_list_push_channels.py b/tests/integrational/native_sync/test_list_push_channels.py index 075492bc..c99875e2 100644 --- a/tests/integrational/native_sync/test_list_push_channels.py +++ b/tests/integrational/native_sync/test_list_push_channels.py @@ -82,25 +82,6 @@ def test_list_push_channels_apns2_basic_success(self): self.assertTrue(envelope.status.is_error() is False) self.assertIsInstance(envelope.result.channels, list) - @pn_vcr.use_cassette( - 'tests/integrational/fixtures/native_sync/list_push_channels/mpns_basic_success.json', - serializer='pn_json', - filter_query_parameters=['seqn', 'pnsdk', 'l_sig'] - ) - def test_list_push_channels_mpns_basic_success(self): - """Test basic MPNS channel listing functionality.""" - device_id = "0000000000000000" - - envelope = self.pubnub.list_push_channels() \ - .device_id(device_id) \ - .push_type(PNPushType.MPNS) \ - .sync() - - self.assertIsNotNone(envelope) - self.assertIsNotNone(envelope.result) - self.assertTrue(envelope.status.is_error() is False) - self.assertIsInstance(envelope.result.channels, list) - @pn_vcr.use_cassette( 'tests/integrational/fixtures/native_sync/list_push_channels/empty_device.json', serializer='pn_json', diff --git a/tests/integrational/native_sync/test_remove_channels_from_push.py b/tests/integrational/native_sync/test_remove_channels_from_push.py index a60bb02c..dadaac1a 100644 --- a/tests/integrational/native_sync/test_remove_channels_from_push.py +++ b/tests/integrational/native_sync/test_remove_channels_from_push.py @@ -81,26 +81,6 @@ def test_remove_channels_from_push_apns2_basic_success(self): self.assertIsNotNone(envelope.result) self.assertTrue(envelope.status.is_error() is False) - @pn_vcr.use_cassette( - 'tests/integrational/fixtures/native_sync/remove_channels_from_push/mpns_basic_success.json', - serializer='pn_json', - filter_query_parameters=['seqn', 'pnsdk', 'l_sig'] - ) - def test_remove_channels_from_push_mpns_basic_success(self): - """Test basic MPNS channel removal functionality.""" - device_id = "0000000000000000" - channels = ["mpns_remove_channel_1", "mpns_remove_channel_2"] - - envelope = self.pubnub.remove_channels_from_push() \ - .channels(channels) \ - .device_id(device_id) \ - .push_type(PNPushType.MPNS) \ - .sync() - - self.assertIsNotNone(envelope) - self.assertIsNotNone(envelope.result) - self.assertTrue(envelope.status.is_error() is False) - @pn_vcr.use_cassette( 'tests/integrational/fixtures/native_sync/remove_channels_from_push/single_channel.json', serializer='pn_json', diff --git a/tests/integrational/native_sync/test_remove_device_from_push.py b/tests/integrational/native_sync/test_remove_device_from_push.py index 3e472e81..de48b04a 100644 --- a/tests/integrational/native_sync/test_remove_device_from_push.py +++ b/tests/integrational/native_sync/test_remove_device_from_push.py @@ -75,24 +75,6 @@ def test_remove_device_from_push_apns2_basic_success(self): self.assertIsNotNone(envelope.result) self.assertTrue(envelope.status.is_error() is False) - @pn_vcr.use_cassette( - 'tests/integrational/fixtures/native_sync/remove_device_from_push/mpns_basic_success.json', - serializer='pn_json', - filter_query_parameters=['seqn', 'pnsdk', 'l_sig'] - ) - def test_remove_device_from_push_mpns_basic_success(self): - """Test basic MPNS device removal functionality.""" - device_id = "0000000000000000" - - envelope = self.pubnub.remove_device_from_push() \ - .device_id(device_id) \ - .push_type(PNPushType.MPNS) \ - .sync() - - self.assertIsNotNone(envelope) - self.assertIsNotNone(envelope.result) - self.assertTrue(envelope.status.is_error() is False) - @pn_vcr.use_cassette( 'tests/integrational/fixtures/native_sync/remove_device_from_push/complete_unregistration.json', serializer='pn_json', diff --git a/tests/pytest.ini b/tests/pytest.ini index 2427aeeb..46573595 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -6,4 +6,6 @@ filterwarnings = ignore:The function .* is deprecated. Use.* Include Object instead:DeprecationWarning ignore:The function .* is deprecated. Use.* PNUserMember class instead:DeprecationWarning -asyncio_default_fixture_loop_scope = module \ No newline at end of file +asyncio_default_fixture_loop_scope = function +timeout = 60 +timeout_func_only = true \ No newline at end of file diff --git a/tests/unit/test_add_channels_to_push.py b/tests/unit/test_add_channels_to_push.py index c1511b7d..d26bf862 100644 --- a/tests/unit/test_add_channels_to_push.py +++ b/tests/unit/test_add_channels_to_push.py @@ -34,7 +34,7 @@ def test_add_channels_to_push_with_named_parameters(self): self.assertEqual(endpoint._topic, topic) self.assertEqual(endpoint._environment, environment) - def test_add_channels_to_push_builder(self): + def test_add_channels_to_push_builder_gcm(self): """Test that the returned object supports method chaining.""" pubnub = PubNub(mocked_config) @@ -50,6 +50,22 @@ def test_add_channels_to_push_builder(self): self.assertEqual(endpoint._device_id, "test_device") self.assertEqual(endpoint._push_type, PNPushType.GCM) + def test_add_channels_to_push_builder_fcm(self): + """Test that the returned object supports method chaining.""" + pubnub = PubNub(mocked_config) + + endpoint = pubnub.add_channels_to_push() \ + .channels(["test_channel"]) \ + .device_id("test_device") \ + .push_type(PNPushType.FCM) \ + .topic("test_topic") \ + .environment(PNPushEnvironment.DEVELOPMENT) + + self.assertIsInstance(endpoint, AddChannelsToPush) + self.assertEqual(endpoint._channels, ["test_channel"]) + self.assertEqual(endpoint._device_id, "test_device") + self.assertEqual(endpoint._push_type, PNPushType.FCM) + def test_add_channels_to_push_apns2_fails_without_topic(self): """Test that APNS2 fails validation when no topic is provided.""" pubnub = PubNub(mocked_config) diff --git a/tests/unit/test_list_push_channels.py b/tests/unit/test_list_push_channels.py index c8e4ba67..5c41f38b 100644 --- a/tests/unit/test_list_push_channels.py +++ b/tests/unit/test_list_push_channels.py @@ -33,7 +33,7 @@ def test_list_push_channels_with_named_parameters(self): self.assertEqual(endpoint._topic, topic) self.assertEqual(endpoint._environment, environment) - def test_list_push_channels_builder(self): + def test_list_push_channels_builder_gcm(self): """Test that the returned object supports method chaining.""" pubnub = PubNub(mocked_config) @@ -47,6 +47,20 @@ def test_list_push_channels_builder(self): self.assertEqual(endpoint._device_id, "test_device") self.assertEqual(endpoint._push_type, PNPushType.GCM) + def test_list_push_channels_builder_fcm(self): + """Test that the returned object supports method chaining.""" + pubnub = PubNub(mocked_config) + + endpoint = pubnub.list_push_channels() \ + .device_id("test_device") \ + .push_type(PNPushType.FCM) \ + .topic("test_topic") \ + .environment(PNPushEnvironment.DEVELOPMENT) + + self.assertIsInstance(endpoint, ListPushProvisions) + self.assertEqual(endpoint._device_id, "test_device") + self.assertEqual(endpoint._push_type, PNPushType.FCM) + def test_list_push_channels_apns2_fails_without_topic(self): """Test that APNS2 fails validation when no topic is provided.""" pubnub = PubNub(mocked_config) diff --git a/tests/unit/test_remove_channels_from_push.py b/tests/unit/test_remove_channels_from_push.py index 01809070..38c7c5b4 100644 --- a/tests/unit/test_remove_channels_from_push.py +++ b/tests/unit/test_remove_channels_from_push.py @@ -34,7 +34,7 @@ def test_remove_channels_from_push_with_named_parameters(self): self.assertEqual(endpoint._topic, topic) self.assertEqual(endpoint._environment, environment) - def test_remove_channels_from_push_builder(self): + def test_remove_channels_from_push_builder_gcm(self): """Test that the returned object supports method chaining.""" pubnub = PubNub(mocked_config) @@ -50,6 +50,22 @@ def test_remove_channels_from_push_builder(self): self.assertEqual(endpoint._device_id, "test_device") self.assertEqual(endpoint._push_type, PNPushType.GCM) + def test_remove_channels_from_push_builder_fcm(self): + """Test that the returned object supports method chaining.""" + pubnub = PubNub(mocked_config) + + endpoint = pubnub.remove_channels_from_push() \ + .channels(["test_channel"]) \ + .device_id("test_device") \ + .push_type(PNPushType.FCM) \ + .topic("test_topic") \ + .environment(PNPushEnvironment.DEVELOPMENT) + + self.assertIsInstance(endpoint, RemoveChannelsFromPush) + self.assertEqual(endpoint._channels, ["test_channel"]) + self.assertEqual(endpoint._device_id, "test_device") + self.assertEqual(endpoint._push_type, PNPushType.FCM) + def test_remove_channels_from_push_apns2_fails_without_topic(self): """Test that APNS2 fails validation when no topic is provided.""" pubnub = PubNub(mocked_config) diff --git a/tests/unit/test_remove_device_from_push.py b/tests/unit/test_remove_device_from_push.py index 2aca152f..d33ef858 100644 --- a/tests/unit/test_remove_device_from_push.py +++ b/tests/unit/test_remove_device_from_push.py @@ -37,13 +37,13 @@ def test_remove_device_from_push_builder(self): endpoint = pubnub.remove_device_from_push() \ .device_id("test_device") \ - .push_type(PNPushType.GCM) \ + .push_type(PNPushType.FCM) \ .topic("test_topic") \ .environment(PNPushEnvironment.DEVELOPMENT) self.assertIsInstance(endpoint, RemoveDeviceFromPush) self.assertEqual(endpoint._device_id, "test_device") - self.assertEqual(endpoint._push_type, PNPushType.GCM) + self.assertEqual(endpoint._push_type, PNPushType.FCM) def test_remove_device_from_push_apns2_fails_without_topic(self): """Test that APNS2 fails validation when no topic is provided."""