Skip to content

Commit a9300ef

Browse files
authored
Implementing support for APNS mutable-content field (#143)
* Implementing support for APNS content-mutable field * Corrected the mutable-content option name * Renamed custom_fields to custom_data; Updated CHANGELOG * Cleaned up tests for clarity
1 parent 9239585 commit a9300ef

File tree

3 files changed

+87
-12
lines changed

3 files changed

+87
-12
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Unreleased
22

3-
-
3+
- [added] Added the `mutable_content` optional field to the `messaging.Aps`
4+
type.
5+
- [added] Added support for specifying arbitrary custom key-value
6+
fields in the `messaging.Aps` type.
47

58
# v2.9.1
69

firebase_admin/messaging.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,16 +343,22 @@ class Aps(object):
343343
notification (optional).
344344
category: String identifier representing the message type (optional).
345345
thread_id: An app-specific string identifier for grouping messages (optional).
346+
mutable_content: A boolean indicating whether to support mutating notifications at
347+
the client using app extensions (optional).
348+
custom_data: A dict of custom key-value pairs to be included in the Aps dictionary
349+
(optional).
346350
"""
347351

348352
def __init__(self, alert=None, badge=None, sound=None, content_available=None, category=None,
349-
thread_id=None):
353+
thread_id=None, mutable_content=None, custom_data=None):
350354
self.alert = alert
351355
self.badge = badge
352356
self.sound = sound
353357
self.content_available = content_available
354358
self.category = category
355359
self.thread_id = thread_id
360+
self.mutable_content = mutable_content
361+
self.custom_data = custom_data
356362

357363

358364
class ApsAlert(object):
@@ -624,6 +630,16 @@ def encode_aps(cls, aps):
624630
}
625631
if aps.content_available is True:
626632
result['content-available'] = 1
633+
if aps.mutable_content is True:
634+
result['mutable-content'] = 1
635+
if aps.custom_data is not None:
636+
if not isinstance(aps.custom_data, dict):
637+
raise ValueError('Aps.custom_data must be a dict.')
638+
for key, val in aps.custom_data.items():
639+
_Validators.check_string('Aps.custom_data key', key)
640+
if key in result:
641+
raise ValueError('Multiple specifications for {0} in Aps.'.format(key))
642+
result[key] = val
627643
return cls.remove_null_values(result)
628644

629645
@classmethod

tests/test_messaging.py

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -537,11 +537,9 @@ def test_apns_payload(self):
537537

538538
class TestApsEncoder(object):
539539

540-
def _check_aps(self, aps):
541-
with pytest.raises(ValueError) as excinfo:
542-
check_encoding(messaging.Message(
543-
topic='topic', apns=messaging.APNSConfig(payload=messaging.APNSPayload(aps=aps))))
544-
return excinfo
540+
def _encode_aps(self, aps):
541+
return check_encoding(messaging.Message(
542+
topic='topic', apns=messaging.APNSConfig(payload=messaging.APNSPayload(aps=aps))))
545543

546544
@pytest.mark.parametrize('data', NON_OBJECT_ARGS)
547545
def test_invalid_aps(self, data):
@@ -555,38 +553,68 @@ def test_invalid_aps(self, data):
555553
@pytest.mark.parametrize('data', NON_STRING_ARGS)
556554
def test_invalid_alert(self, data):
557555
aps = messaging.Aps(alert=data)
558-
excinfo = self._check_aps(aps)
556+
with pytest.raises(ValueError) as excinfo:
557+
self._encode_aps(aps)
559558
expected = 'Aps.alert must be a string or an instance of ApsAlert class.'
560559
assert str(excinfo.value) == expected
561560

562561
@pytest.mark.parametrize('data', [list(), tuple(), dict(), 'foo'])
563562
def test_invalid_badge(self, data):
564563
aps = messaging.Aps(badge=data)
565-
excinfo = self._check_aps(aps)
564+
with pytest.raises(ValueError) as excinfo:
565+
self._encode_aps(aps)
566566
expected = 'Aps.badge must be a number.'
567567
assert str(excinfo.value) == expected
568568

569569
@pytest.mark.parametrize('data', NON_STRING_ARGS)
570570
def test_invalid_sound(self, data):
571571
aps = messaging.Aps(sound=data)
572-
excinfo = self._check_aps(aps)
572+
with pytest.raises(ValueError) as excinfo:
573+
self._encode_aps(aps)
573574
expected = 'Aps.sound must be a string.'
574575
assert str(excinfo.value) == expected
575576

576577
@pytest.mark.parametrize('data', NON_STRING_ARGS)
577578
def test_invalid_category(self, data):
578579
aps = messaging.Aps(category=data)
579-
excinfo = self._check_aps(aps)
580+
with pytest.raises(ValueError) as excinfo:
581+
self._encode_aps(aps)
580582
expected = 'Aps.category must be a string.'
581583
assert str(excinfo.value) == expected
582584

583585
@pytest.mark.parametrize('data', NON_STRING_ARGS)
584586
def test_invalid_thread_id(self, data):
585587
aps = messaging.Aps(thread_id=data)
586-
excinfo = self._check_aps(aps)
588+
with pytest.raises(ValueError) as excinfo:
589+
self._encode_aps(aps)
587590
expected = 'Aps.thread_id must be a string.'
588591
assert str(excinfo.value) == expected
589592

593+
@pytest.mark.parametrize('data', ['', list(), tuple(), True, False, 1, 0, ])
594+
def test_invalid_custom_data_dict(self, data):
595+
if isinstance(data, dict):
596+
return
597+
aps = messaging.Aps(custom_data=data)
598+
with pytest.raises(ValueError) as excinfo:
599+
self._encode_aps(aps)
600+
expected = 'Aps.custom_data must be a dict.'
601+
assert str(excinfo.value) == expected
602+
603+
@pytest.mark.parametrize('data', [True, False, 1, 0])
604+
def test_invalid_custom_field_name(self, data):
605+
aps = messaging.Aps(custom_data={data: 'foo'})
606+
with pytest.raises(ValueError) as excinfo:
607+
self._encode_aps(aps)
608+
expected = 'Aps.custom_data key must be a string.'
609+
assert str(excinfo.value) == expected
610+
611+
def test_multiple_field_specifications(self):
612+
aps = messaging.Aps(thread_id='foo', custom_data={'thread-id': 'foo'})
613+
with pytest.raises(ValueError) as excinfo:
614+
self._encode_aps(aps)
615+
expected = 'Multiple specifications for thread-id in Aps.'
616+
assert str(excinfo.value) == expected
617+
590618
def test_aps(self):
591619
msg = messaging.Message(
592620
topic='topic',
@@ -597,6 +625,7 @@ def test_aps(self):
597625
badge=42,
598626
sound='s',
599627
content_available=True,
628+
mutable_content=True,
600629
category='c',
601630
thread_id='t'
602631
),
@@ -612,6 +641,7 @@ def test_aps(self):
612641
'badge': 42,
613642
'sound': 's',
614643
'content-available': 1,
644+
'mutable-content': 1,
615645
'category': 'c',
616646
'thread-id': 't',
617647
},
@@ -620,6 +650,32 @@ def test_aps(self):
620650
}
621651
check_encoding(msg, expected)
622652

653+
def test_aps_custom_data(self):
654+
msg = messaging.Message(
655+
topic='topic',
656+
apns=messaging.APNSConfig(
657+
payload=messaging.APNSPayload(
658+
aps=messaging.Aps(
659+
alert='alert text',
660+
custom_data={'k1': 'v1', 'k2': 1},
661+
),
662+
)
663+
)
664+
)
665+
expected = {
666+
'topic': 'topic',
667+
'apns': {
668+
'payload': {
669+
'aps': {
670+
'alert': 'alert text',
671+
'k1': 'v1',
672+
'k2': 1,
673+
},
674+
}
675+
},
676+
}
677+
check_encoding(msg, expected)
678+
623679

624680
class TestApsAlertEncoder(object):
625681

0 commit comments

Comments
 (0)