From 88565d4f086088ba4e1a5bf74660bba761237c8f Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Sat, 29 Jun 2019 00:06:28 +0200 Subject: [PATCH 1/6] add analytics_label in FcmOptions --- firebase_admin/_messaging_utils.py | 42 +++++++++++++++++++++++++++--- firebase_admin/messaging.py | 7 +++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/firebase_admin/_messaging_utils.py b/firebase_admin/_messaging_utils.py index 17067f175..590fb0054 100644 --- a/firebase_admin/_messaging_utils.py +++ b/firebase_admin/_messaging_utils.py @@ -36,6 +36,7 @@ class Message(object): android: An instance of ``messaging.AndroidConfig`` (optional). webpush: An instance of ``messaging.WebpushConfig`` (optional). apns: An instance of ``messaging.ApnsConfig`` (optional). + fcm_options: An instance of ``messaging.FcmOptions`` (optional). token: The registration token of the device to which the message should be sent (optional). topic: Name of the FCM topic to which the message should be sent (optional). Topic name may contain the ``/topics/`` prefix. @@ -43,12 +44,13 @@ class Message(object): """ def __init__(self, data=None, notification=None, android=None, webpush=None, apns=None, - token=None, topic=None, condition=None): + fcm_options=None, token=None, topic=None, condition=None): self.data = data self.notification = notification self.android = android self.webpush = webpush self.apns = apns + self.fcm_options = fcm_options self.token = token self.topic = topic self.condition = condition @@ -65,8 +67,10 @@ class MulticastMessage(object): android: An instance of ``messaging.AndroidConfig`` (optional). webpush: An instance of ``messaging.WebpushConfig`` (optional). apns: An instance of ``messaging.ApnsConfig`` (optional). + fcm_options: An instance of ``messaging.FcmOptions`` (optional). """ - def __init__(self, tokens, data=None, notification=None, android=None, webpush=None, apns=None): + def __init__(self, tokens, data=None, notification=None, android=None, webpush=None, apns=None, + fcm_options=None): _Validators.check_string_list('MulticastMessage.tokens', tokens) if len(tokens) > 100: raise ValueError('MulticastMessage.tokens must not contain more than 100 tokens.') @@ -76,6 +80,7 @@ def __init__(self, tokens, data=None, notification=None, android=None, webpush=N self.android = android self.webpush = webpush self.apns = apns + self.fcm_options = fcm_options class Notification(object): @@ -107,16 +112,18 @@ class AndroidConfig(object): data: A dictionary of data fields (optional). All keys and values in the dictionary must be strings. When specified, overrides any data fields set via ``Message.data``. notification: A ``messaging.AndroidNotification`` to be included in the message (optional). + fcm_options: A ``messaging.AndroidFcmOptions`` to be included in the message (optional). """ def __init__(self, collapse_key=None, priority=None, ttl=None, restricted_package_name=None, - data=None, notification=None): + data=None, notification=None, fcm_options=None): self.collapse_key = collapse_key self.priority = priority self.ttl = ttl self.restricted_package_name = restricted_package_name self.data = data self.notification = notification + self.fcm_options = fcm_options class AndroidNotification(object): @@ -165,6 +172,18 @@ def __init__(self, title=None, body=None, icon=None, color=None, sound=None, tag self.channel_id = channel_id +class AndroidFcmOptions(object): + """Options for features provided by the FCM SDK for Android. + + Args: + analytics_label: contains additional options for features provided by the FCM Android SDK + (optional). + """ + + def __init__(self, analytics_label=None): + self.analytics_label = analytics_label + + class WebpushConfig(object): """Webpush-specific options that can be included in a message. @@ -279,14 +298,17 @@ class APNSConfig(object): Args: headers: A dictionary of headers (optional). payload: A ``messaging.APNSPayload`` to be included in the message (optional). + fcm_options: A ``messaging.APNSFcmOptions`` instance to be included in the message + (optional). .. _APNS Documentation: https://developer.apple.com/library/content/documentation\ /NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html """ - def __init__(self, headers=None, payload=None): + def __init__(self, headers=None, payload=None, fcm_options=None): self.headers = headers self.payload = payload + self.fcm_options = fcm_options class APNSPayload(object): @@ -387,6 +409,18 @@ def __init__(self, title=None, subtitle=None, body=None, loc_key=None, loc_args= self.launch_image = launch_image +class APNSFcmOptions(object): + """Options for features provided by the FCM SDK for iOS. + + Args: + analytics_label: contains additional options for features provided by the FCM Android SDK + (optional). + """ + + def __init__(self, analytics_label=None): + self.analytics_label = analytics_label + + class _Validators(object): """A collection of data validation utilities. diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index 35d9e4ccd..d5c9510f2 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -33,8 +33,10 @@ __all__ = [ 'AndroidConfig', + 'AndroidFcmOptions', 'AndroidNotification', 'APNSConfig', + 'APNSFcmOptions', 'APNSPayload', 'ApiCallError', 'Aps', @@ -42,6 +44,7 @@ 'BatchResponse', 'CriticalSound', 'ErrorInfo', + 'FcmOptions', 'Message', 'MulticastMessage', 'Notification', @@ -61,12 +64,15 @@ AndroidConfig = _messaging_utils.AndroidConfig +AndroidFcmOptions = _messaging_utils.AndroidFcmOptions AndroidNotification = _messaging_utils.AndroidNotification APNSConfig = _messaging_utils.APNSConfig +APNSFcmOptions = _messaging_utils.APNSFcmOptions APNSPayload = _messaging_utils.APNSPayload Aps = _messaging_utils.Aps ApsAlert = _messaging_utils.ApsAlert CriticalSound = _messaging_utils.CriticalSound +FcmOptions = _messaging_utils.FcmOptions Message = _messaging_utils.Message MulticastMessage = _messaging_utils.MulticastMessage Notification = _messaging_utils.Notification @@ -145,6 +151,7 @@ def send_multicast(multicast_message, dry_run=False, app=None): android=multicast_message.android, webpush=multicast_message.webpush, apns=multicast_message.apns, + fcmOptions=multicast_message.fcmOptions, token=token ) for token in multicast_message.tokens] return _get_messaging_service(app).send_all(messages, dry_run) From d245452f9213c8a2c0352b2c2b2320d4f16ce645 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Sat, 29 Jun 2019 00:21:35 +0200 Subject: [PATCH 2/6] fix errors --- firebase_admin/_messaging_utils.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/firebase_admin/_messaging_utils.py b/firebase_admin/_messaging_utils.py index 590fb0054..514fdc509 100644 --- a/firebase_admin/_messaging_utils.py +++ b/firebase_admin/_messaging_utils.py @@ -413,13 +413,22 @@ class APNSFcmOptions(object): """Options for features provided by the FCM SDK for iOS. Args: - analytics_label: contains additional options for features provided by the FCM Android SDK + analytics_label: contains additional options for features provided by the FCM iOS SDK (optional). """ def __init__(self, analytics_label=None): self.analytics_label = analytics_label +class FcmOptions(object): + """Options for features provided by SDK. + + Args: + analytics_label: contains additional options to use across all platforms (optional). + """ + + def __init__(self, analytics_label=None): + self.analytics_label = analytics_label class _Validators(object): """A collection of data validation utilities. @@ -587,7 +596,7 @@ def encode_webpush(cls, webpush): 'headers': _Validators.check_string_dict( 'WebpushConfig.headers', webpush.headers), 'notification': cls.encode_webpush_notification(webpush.notification), - 'fcmOptions': cls.encode_webpush_fcm_options(webpush.fcm_options), + 'fcm_options': cls.encode_webpush_fcm_options(webpush.fcm_options), } return cls.remove_null_values(result) From 4071231aa9419c5f8336d0712824224780169241 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Sat, 29 Jun 2019 00:21:35 +0200 Subject: [PATCH 3/6] fix errors --- firebase_admin/_messaging_utils.py | 13 +++++++++++-- firebase_admin/messaging.py | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/firebase_admin/_messaging_utils.py b/firebase_admin/_messaging_utils.py index 590fb0054..514fdc509 100644 --- a/firebase_admin/_messaging_utils.py +++ b/firebase_admin/_messaging_utils.py @@ -413,13 +413,22 @@ class APNSFcmOptions(object): """Options for features provided by the FCM SDK for iOS. Args: - analytics_label: contains additional options for features provided by the FCM Android SDK + analytics_label: contains additional options for features provided by the FCM iOS SDK (optional). """ def __init__(self, analytics_label=None): self.analytics_label = analytics_label +class FcmOptions(object): + """Options for features provided by SDK. + + Args: + analytics_label: contains additional options to use across all platforms (optional). + """ + + def __init__(self, analytics_label=None): + self.analytics_label = analytics_label class _Validators(object): """A collection of data validation utilities. @@ -587,7 +596,7 @@ def encode_webpush(cls, webpush): 'headers': _Validators.check_string_dict( 'WebpushConfig.headers', webpush.headers), 'notification': cls.encode_webpush_notification(webpush.notification), - 'fcmOptions': cls.encode_webpush_fcm_options(webpush.fcm_options), + 'fcm_options': cls.encode_webpush_fcm_options(webpush.fcm_options), } return cls.remove_null_values(result) diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index d5c9510f2..bb5431587 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -151,7 +151,7 @@ def send_multicast(multicast_message, dry_run=False, app=None): android=multicast_message.android, webpush=multicast_message.webpush, apns=multicast_message.apns, - fcmOptions=multicast_message.fcmOptions, + fcm_options=multicast_message.fcmOptions, token=token ) for token in multicast_message.tokens] return _get_messaging_service(app).send_all(messages, dry_run) From f7018fd0b89c19921aea199fc40fc116c4193f02 Mon Sep 17 00:00:00 2001 From: wei wang Date: Fri, 19 Jul 2019 11:30:12 +0800 Subject: [PATCH 4/6] add analytics_label encoders --- firebase_admin/_messaging_utils.py | 50 ++++++++++++++++++++++ firebase_admin/messaging.py | 2 +- tests/test_messaging.py | 68 ++++++++++++++++++++++++++++-- 3 files changed, 116 insertions(+), 4 deletions(-) diff --git a/firebase_admin/_messaging_utils.py b/firebase_admin/_messaging_utils.py index 514fdc509..495271e7c 100644 --- a/firebase_admin/_messaging_utils.py +++ b/firebase_admin/_messaging_utils.py @@ -420,6 +420,7 @@ class APNSFcmOptions(object): def __init__(self, analytics_label=None): self.analytics_label = analytics_label + class FcmOptions(object): """Options for features provided by SDK. @@ -430,6 +431,7 @@ class FcmOptions(object): def __init__(self, analytics_label=None): self.analytics_label = analytics_label + class _Validators(object): """A collection of data validation utilities. @@ -485,6 +487,13 @@ def check_string_list(cls, label, value): raise ValueError('{0} must not contain non-string values.'.format(label)) return value + @classmethod + def check_analytics_label(cls, label, value): + value = _Validators.check_string(label, value) + if value is not None and not re.match(r'^[a-zA-Z0-9-_.~%]{1,50}$', value): + raise ValueError('Malformed {}.'.format(label)) + return value + class MessageEncoder(json.JSONEncoder): """A custom JSONEncoder implementation for serializing Message instances into JSON.""" @@ -511,6 +520,7 @@ def encode_android(cls, android): 'restricted_package_name': _Validators.check_string( 'AndroidConfig.restricted_package_name', android.restricted_package_name), 'ttl': cls.encode_ttl(android.ttl), + 'fcm_options': cls.encode_android_fcm_options(android.fcm_options), } result = cls.remove_null_values(result) priority = result.get('priority') @@ -518,6 +528,20 @@ def encode_android(cls, android): raise ValueError('AndroidConfig.priority must be "high" or "normal".') return result + @classmethod + def encode_android_fcm_options(cls, fcm_options): + """Encodes a AndroidFcmOptions instance into a json.""" + if fcm_options is None: + return None + if not isinstance(fcm_options, AndroidFcmOptions): + raise ValueError('AndroidConfig.fcm_options must be an instance of ' + 'AndroidFcmOptions class.') + result = { + 'analytics_label': _Validators.check_analytics_label('AndroidFcmOptions.analytics_label', fcm_options.analytics_label), + } + result = cls.remove_null_values(result) + return result + @classmethod def encode_ttl(cls, ttl): """Encodes a AndroidConfig TTL duration into a string.""" @@ -696,6 +720,7 @@ def encode_apns(cls, apns): 'headers': _Validators.check_string_dict( 'APNSConfig.headers', apns.headers), 'payload': cls.encode_apns_payload(apns.payload), + 'fcm_options': cls.encode_apns_fcm_options(apns.fcm_options), } return cls.remove_null_values(result) @@ -713,6 +738,18 @@ def encode_apns_payload(cls, payload): result[key] = value return cls.remove_null_values(result) + @classmethod + def encode_apns_fcm_options(cls, fcm_options): + if fcm_options is None: + return None + if not isinstance(fcm_options, APNSFcmOptions): + raise ValueError('APNSConfig.fcm_options must be an instance of APNSFcmOptions class.') + result = { + 'analytics_label': _Validators.check_analytics_label('APNSFcmOptions.analytics_label', fcm_options.analytics_label), + } + result = cls.remove_null_values(result) + return result + @classmethod def encode_aps(cls, aps): """Encodes an Aps instance into JSON.""" @@ -833,6 +870,7 @@ def default(self, obj): # pylint: disable=method-hidden 'token': _Validators.check_string('Message.token', obj.token, non_empty=True), 'topic': _Validators.check_string('Message.topic', obj.topic, non_empty=True), 'webpush': MessageEncoder.encode_webpush(obj.webpush), + 'fcm_options': MessageEncoder.encode_fcm_options(obj.fcm_options), } result['topic'] = MessageEncoder.sanitize_topic_name(result.get('topic')) result = MessageEncoder.remove_null_values(result) @@ -840,3 +878,15 @@ def default(self, obj): # pylint: disable=method-hidden if target_count != 1: raise ValueError('Exactly one of token, topic or condition must be specified.') return result + + @classmethod + def encode_fcm_options(cls, fcm_options): + if fcm_options is None: + return None + if not isinstance(fcm_options, FcmOptions): + raise ValueError('Message.fcm_options must be an instance of FcmOptions class.') + result = { + 'analytics_label': _Validators.check_analytics_label('FcmOptions.analytics_label', fcm_options.analytics_label), + } + result = cls.remove_null_values(result) + return result diff --git a/firebase_admin/messaging.py b/firebase_admin/messaging.py index bb5431587..ddaef19f0 100644 --- a/firebase_admin/messaging.py +++ b/firebase_admin/messaging.py @@ -151,7 +151,7 @@ def send_multicast(multicast_message, dry_run=False, app=None): android=multicast_message.android, webpush=multicast_message.webpush, apns=multicast_message.apns, - fcm_options=multicast_message.fcmOptions, + fcm_options=multicast_message.fcm_options, token=token ) for token in multicast_message.tokens] return _get_messaging_service(app).send_all(messages, dry_run) diff --git a/tests/test_messaging.py b/tests/test_messaging.py index de940b591..a8456b712 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -120,6 +120,14 @@ def test_data_message(self): def test_prefixed_topic(self): check_encoding(messaging.Message(topic='/topics/topic'), {'topic': 'topic'}) + def test_fcm_options(self): + check_encoding( + messaging.Message(topic='topic', fcm_options=messaging.FcmOptions('analytics_label_v1')), + {'topic': 'topic', 'fcm_options': {'analytics_label': 'analytics_label_v1'}}) + check_encoding( + messaging.Message(topic='topic', fcm_options=messaging.FcmOptions()), + {'topic': 'topic'}) + class TestNotificationEncoder(object): @@ -157,6 +165,50 @@ def test_notification_message(self): {'topic': 'topic', 'notification': {'title': 't'}}) +class TestFcmOptionEncoder(object): + + @pytest.mark.parametrize('label', [ + '!', + 'THIS_IS_LONGER_THAN_50_CHARACTERS_WHICH_IS_NOT_ALLOWED', + '', + ]) + def test_invalid_fcm_options(self, label): + with pytest.raises(ValueError) as excinfo: + check_encoding(messaging.Message( + topic='topic', + fcm_options=messaging.FcmOptions(label) + )) + expected = 'Malformed FcmOptions.analytics_label.' + assert str(excinfo.value) == expected + + def test_fcm_options(self): + check_encoding( + messaging.Message( + topic='topic', + fcm_options=messaging.FcmOptions(), + android=messaging.AndroidConfig(fcm_options=messaging.AndroidFcmOptions()), + apns=messaging.APNSConfig(fcm_options=messaging.APNSFcmOptions()) + ), + {'topic': 'topic'}) + check_encoding( + messaging.Message( + topic='topic', + fcm_options=messaging.FcmOptions('message-label'), + android=messaging.AndroidConfig(fcm_options=messaging.AndroidFcmOptions('android-label')), + apns=messaging.APNSConfig(fcm_options=messaging.APNSFcmOptions('apns-label')) + ), + { + 'topic': 'topic', + 'fcm_options': {'analytics_label': 'message-label'}, + 'android': { + 'fcm_options': {'analytics_label': 'android-label'}, + }, + 'apns': { + 'fcm_options': {'analytics_label': 'apns-label'}, + }, + }) + + class TestAndroidConfigEncoder(object): @pytest.mark.parametrize('data', NON_OBJECT_ARGS) @@ -216,7 +268,8 @@ def test_android_config(self): restricted_package_name='package', priority='high', ttl=123, - data={'k1': 'v1', 'k2': 'v2'} + data={'k1': 'v1', 'k2': 'v2'}, + fcm_options=messaging.AndroidFcmOptions('analytics_label_v1') ) ) expected = { @@ -230,6 +283,9 @@ def test_android_config(self): 'k1': 'v1', 'k2': 'v2', }, + 'fcm_options': { + 'analytics_label': 'analytics_label_v1', + }, }, } check_encoding(msg, expected) @@ -484,7 +540,7 @@ def test_webpush_notification(self): expected = { 'topic': 'topic', 'webpush': { - 'fcmOptions': { + 'fcm_options': { 'link': 'https://example', }, }, @@ -714,7 +770,10 @@ def test_invalid_headers(self, data): def test_apns_config(self): msg = messaging.Message( topic='topic', - apns=messaging.APNSConfig(headers={'h1': 'v1', 'h2': 'v2'}) + apns=messaging.APNSConfig( + headers={'h1': 'v1', 'h2': 'v2'}, + fcm_options=messaging.APNSFcmOptions('analytics_label_v1') + ), ) expected = { 'topic': 'topic', @@ -723,6 +782,9 @@ def test_apns_config(self): 'h1': 'v1', 'h2': 'v2', }, + 'fcm_options': { + 'analytics_label': 'analytics_label_v1', + }, }, } check_encoding(msg, expected) From 539e5c33e00e8dc84fe9af21d8920255fc7520b0 Mon Sep 17 00:00:00 2001 From: wei wang Date: Fri, 19 Jul 2019 11:53:22 +0800 Subject: [PATCH 5/6] fix line-too-long --- firebase_admin/_messaging_utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/firebase_admin/_messaging_utils.py b/firebase_admin/_messaging_utils.py index 495271e7c..c728edf6f 100644 --- a/firebase_admin/_messaging_utils.py +++ b/firebase_admin/_messaging_utils.py @@ -537,7 +537,8 @@ def encode_android_fcm_options(cls, fcm_options): raise ValueError('AndroidConfig.fcm_options must be an instance of ' 'AndroidFcmOptions class.') result = { - 'analytics_label': _Validators.check_analytics_label('AndroidFcmOptions.analytics_label', fcm_options.analytics_label), + 'analytics_label': _Validators.check_analytics_label( + 'AndroidFcmOptions.analytics_label', fcm_options.analytics_label), } result = cls.remove_null_values(result) return result @@ -745,7 +746,8 @@ def encode_apns_fcm_options(cls, fcm_options): if not isinstance(fcm_options, APNSFcmOptions): raise ValueError('APNSConfig.fcm_options must be an instance of APNSFcmOptions class.') result = { - 'analytics_label': _Validators.check_analytics_label('APNSFcmOptions.analytics_label', fcm_options.analytics_label), + 'analytics_label': + _Validators.check_analytics_label('APNSFcmOptions.analytics_label', fcm_options.analytics_label), } result = cls.remove_null_values(result) return result @@ -886,7 +888,8 @@ def encode_fcm_options(cls, fcm_options): if not isinstance(fcm_options, FcmOptions): raise ValueError('Message.fcm_options must be an instance of FcmOptions class.') result = { - 'analytics_label': _Validators.check_analytics_label('FcmOptions.analytics_label', fcm_options.analytics_label), + 'analytics_label': _Validators.check_analytics_label( + 'FcmOptions.analytics_label', fcm_options.analytics_label), } result = cls.remove_null_values(result) return result From f76863fa7cdbc676f4ecf6865be435224e41dcf2 Mon Sep 17 00:00:00 2001 From: wei wang Date: Fri, 19 Jul 2019 12:13:56 +0800 Subject: [PATCH 6/6] fix lint errors --- firebase_admin/_messaging_utils.py | 7 +++++-- tests/test_messaging.py | 18 ++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/firebase_admin/_messaging_utils.py b/firebase_admin/_messaging_utils.py index c728edf6f..09f7daf87 100644 --- a/firebase_admin/_messaging_utils.py +++ b/firebase_admin/_messaging_utils.py @@ -489,6 +489,7 @@ def check_string_list(cls, label, value): @classmethod def check_analytics_label(cls, label, value): + """Checks if the given value is a valid analytics label.""" value = _Validators.check_string(label, value) if value is not None and not re.match(r'^[a-zA-Z0-9-_.~%]{1,50}$', value): raise ValueError('Malformed {}.'.format(label)) @@ -741,13 +742,14 @@ def encode_apns_payload(cls, payload): @classmethod def encode_apns_fcm_options(cls, fcm_options): + """Encodes an APNSFcmOptions instance into JSON.""" if fcm_options is None: return None if not isinstance(fcm_options, APNSFcmOptions): raise ValueError('APNSConfig.fcm_options must be an instance of APNSFcmOptions class.') result = { - 'analytics_label': - _Validators.check_analytics_label('APNSFcmOptions.analytics_label', fcm_options.analytics_label), + 'analytics_label': _Validators.check_analytics_label( + 'APNSFcmOptions.analytics_label', fcm_options.analytics_label), } result = cls.remove_null_values(result) return result @@ -883,6 +885,7 @@ def default(self, obj): # pylint: disable=method-hidden @classmethod def encode_fcm_options(cls, fcm_options): + """Encodes an FcmOptions instance into JSON.""" if fcm_options is None: return None if not isinstance(fcm_options, FcmOptions): diff --git a/tests/test_messaging.py b/tests/test_messaging.py index a8456b712..878e1365b 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -122,7 +122,8 @@ def test_prefixed_topic(self): def test_fcm_options(self): check_encoding( - messaging.Message(topic='topic', fcm_options=messaging.FcmOptions('analytics_label_v1')), + messaging.Message( + topic='topic', fcm_options=messaging.FcmOptions('analytics_label_v1')), {'topic': 'topic', 'fcm_options': {'analytics_label': 'analytics_label_v1'}}) check_encoding( messaging.Message(topic='topic', fcm_options=messaging.FcmOptions()), @@ -194,19 +195,16 @@ def test_fcm_options(self): messaging.Message( topic='topic', fcm_options=messaging.FcmOptions('message-label'), - android=messaging.AndroidConfig(fcm_options=messaging.AndroidFcmOptions('android-label')), + android=messaging.AndroidConfig( + fcm_options=messaging.AndroidFcmOptions('android-label')), apns=messaging.APNSConfig(fcm_options=messaging.APNSFcmOptions('apns-label')) ), { 'topic': 'topic', - 'fcm_options': {'analytics_label': 'message-label'}, - 'android': { - 'fcm_options': {'analytics_label': 'android-label'}, - }, - 'apns': { - 'fcm_options': {'analytics_label': 'apns-label'}, - }, - }) + 'fcm_options': {'analytics_label': 'message-label'}, + 'android': {'fcm_options': {'analytics_label': 'android-label'}}, + 'apns': {'fcm_options': {'analytics_label': 'apns-label'}}, + }) class TestAndroidConfigEncoder(object):