From 6651302cb91f22e880aa544c1896879d3732c098 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Tue, 24 Oct 2023 11:30:25 -0400 Subject: [PATCH 1/8] feat(fcm): Enabled direct_boot_ok Android Config parameter. --- firebase_admin/_messaging_encoder.py | 11 +++++++++++ firebase_admin/_messaging_utils.py | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/firebase_admin/_messaging_encoder.py b/firebase_admin/_messaging_encoder.py index 48a3dd3cd..725db45cc 100644 --- a/firebase_admin/_messaging_encoder.py +++ b/firebase_admin/_messaging_encoder.py @@ -160,6 +160,15 @@ def check_analytics_label(cls, label, value): raise ValueError('Malformed {}.'.format(label)) return value + @classmethod + def check_boolean(cls, label, value): + """Checks if the given value is boolean.""" + if value is None: + return None + if not isinstance(value, bool): + raise ValueError('{0} must be a boolean.'.format(label)) + return value + @classmethod def check_datetime(cls, label, value): """Checks if the given value is a datetime.""" @@ -214,6 +223,8 @@ def encode_android_fcm_options(cls, fcm_options): result = { 'analytics_label': _Validators.check_analytics_label( 'AndroidFCMOptions.analytics_label', fcm_options.analytics_label), + 'direct_boot_ok': _Validators.check_boolean( + 'AndroidFCMOptions.direct_boot_ok', fcm_options.direct_boot_ok) } result = cls.remove_null_values(result) return result diff --git a/firebase_admin/_messaging_utils.py b/firebase_admin/_messaging_utils.py index 64930f1b8..e10b869bd 100644 --- a/firebase_admin/_messaging_utils.py +++ b/firebase_admin/_messaging_utils.py @@ -203,10 +203,13 @@ class AndroidFCMOptions: Args: analytics_label: contains additional options for features provided by the FCM Android SDK (optional). + direct_boot_ok: A boolean indicating whether messages will be allowed to be delivered to + the app while the device is in direct boot mode. """ - def __init__(self, analytics_label=None): + def __init__(self, analytics_label=None, direct_boot_ok=None): self.analytics_label = analytics_label + self.direct_boot_ok = direct_boot_ok class WebpushConfig: From 9288589fcd931a4f53a413e037071f3d3e224b92 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Tue, 24 Oct 2023 11:32:51 -0400 Subject: [PATCH 2/8] Added tests. --- tests/test_messaging.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 71bb13eed..895448e9e 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -33,6 +33,7 @@ NON_OBJECT_ARGS = [list(), tuple(), dict(), 'foo', 0, 1, True, False] NON_LIST_ARGS = ['', tuple(), dict(), True, False, 1, 0, [1], ['foo', 1]] NON_UINT_ARGS = ['1.23s', list(), tuple(), dict(), -1.23] +NON_BOOL_ARGS = ['', list(), tuple(), dict(), 1, 0, [1], ['foo', 1], {1: 'foo'}, {'foo': 1}] HTTP_ERROR_CODES = { 400: exceptions.InvalidArgumentError, 403: exceptions.PermissionDeniedError, @@ -249,7 +250,7 @@ def test_fcm_options(self): topic='topic', fcm_options=messaging.FCMOptions('message-label'), android=messaging.AndroidConfig( - fcm_options=messaging.AndroidFCMOptions('android-label')), + fcm_options=messaging.AndroidFCMOptions('android-label', False)), apns=messaging.APNSConfig(fcm_options= messaging.APNSFCMOptions( analytics_label='apns-label', @@ -259,7 +260,8 @@ def test_fcm_options(self): { 'topic': 'topic', 'fcm_options': {'analytics_label': 'message-label'}, - 'android': {'fcm_options': {'analytics_label': 'android-label'}}, + 'android': {'fcm_options': {'analytics_label': 'android-label', + 'direct_boot_ok': False,}}, 'apns': {'fcm_options': {'analytics_label': 'apns-label', 'image': 'https://images.unsplash.com/photo-14944386399' '46-1ebd1d20bf85?fit=crop&w=900&q=60'}}, @@ -317,6 +319,21 @@ def test_invalid_data(self, data): check_encoding(messaging.Message( topic='topic', android=messaging.AndroidConfig(data=data))) + @pytest.mark.parametrize('data', NON_STRING_ARGS) + def test_invalid_analytics_label(self, data): + with pytest.raises(ValueError): + check_encoding(messaging.Message( + topic='topic', android=messaging.AndroidConfig( + fcm_options=messaging.AndroidFCMOptions(analytics_label=data)))) + + @pytest.mark.parametrize('data', NON_BOOL_ARGS) + def test_invalid_direct_boot_ok(self, data): + with pytest.raises(ValueError): + check_encoding(messaging.Message( + topic='topic', android=messaging.AndroidConfig( + fcm_options=messaging.AndroidFCMOptions(direct_boot_ok=data)))) + + def test_android_config(self): msg = messaging.Message( topic='topic', @@ -326,7 +343,7 @@ def test_android_config(self): priority='high', ttl=123, data={'k1': 'v1', 'k2': 'v2'}, - fcm_options=messaging.AndroidFCMOptions('analytics_label_v1') + fcm_options=messaging.AndroidFCMOptions('analytics_label_v1', True) ) ) expected = { @@ -342,6 +359,7 @@ def test_android_config(self): }, 'fcm_options': { 'analytics_label': 'analytics_label_v1', + 'direct_boot_ok': True, }, }, } From fb83427b5b1666c3fe20c14951291ee17ca74ec5 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Tue, 24 Oct 2023 12:19:42 -0400 Subject: [PATCH 3/8] fix: add to correct config. --- firebase_admin/_messaging_encoder.py | 4 ++-- firebase_admin/_messaging_utils.py | 10 +++++----- tests/test_messaging.py | 15 ++++++++------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/firebase_admin/_messaging_encoder.py b/firebase_admin/_messaging_encoder.py index 725db45cc..9910e5541 100644 --- a/firebase_admin/_messaging_encoder.py +++ b/firebase_admin/_messaging_encoder.py @@ -205,6 +205,8 @@ def encode_android(cls, android): 'AndroidConfig.restricted_package_name', android.restricted_package_name), 'ttl': cls.encode_ttl(android.ttl), 'fcm_options': cls.encode_android_fcm_options(android.fcm_options), + 'direct_boot_ok': _Validators.check_boolean( + 'AndroidFCMOptions.direct_boot_ok', android.direct_boot_ok), } result = cls.remove_null_values(result) priority = result.get('priority') @@ -223,8 +225,6 @@ def encode_android_fcm_options(cls, fcm_options): result = { 'analytics_label': _Validators.check_analytics_label( 'AndroidFCMOptions.analytics_label', fcm_options.analytics_label), - 'direct_boot_ok': _Validators.check_boolean( - 'AndroidFCMOptions.direct_boot_ok', fcm_options.direct_boot_ok) } result = cls.remove_null_values(result) return result diff --git a/firebase_admin/_messaging_utils.py b/firebase_admin/_messaging_utils.py index e10b869bd..29b8276bc 100644 --- a/firebase_admin/_messaging_utils.py +++ b/firebase_admin/_messaging_utils.py @@ -49,10 +49,12 @@ class AndroidConfig: 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). + direct_boot_ok: A boolean indicating whether messages will be allowed to be delivered to + the app while the device is in direct boot mode (optional). """ def __init__(self, collapse_key=None, priority=None, ttl=None, restricted_package_name=None, - data=None, notification=None, fcm_options=None): + data=None, notification=None, fcm_options=None, direct_boot_ok=None): self.collapse_key = collapse_key self.priority = priority self.ttl = ttl @@ -60,6 +62,7 @@ def __init__(self, collapse_key=None, priority=None, ttl=None, restricted_packag self.data = data self.notification = notification self.fcm_options = fcm_options + self.direct_boot_ok = direct_boot_ok class AndroidNotification: @@ -203,13 +206,10 @@ class AndroidFCMOptions: Args: analytics_label: contains additional options for features provided by the FCM Android SDK (optional). - direct_boot_ok: A boolean indicating whether messages will be allowed to be delivered to - the app while the device is in direct boot mode. """ - def __init__(self, analytics_label=None, direct_boot_ok=None): + def __init__(self, analytics_label=None): self.analytics_label = analytics_label - self.direct_boot_ok = direct_boot_ok class WebpushConfig: diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 895448e9e..5072df6ea 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -250,7 +250,8 @@ def test_fcm_options(self): topic='topic', fcm_options=messaging.FCMOptions('message-label'), android=messaging.AndroidConfig( - fcm_options=messaging.AndroidFCMOptions('android-label', False)), + fcm_options=messaging.AndroidFCMOptions('android-label'), + direct_boot_ok=False), apns=messaging.APNSConfig(fcm_options= messaging.APNSFCMOptions( analytics_label='apns-label', @@ -260,8 +261,8 @@ def test_fcm_options(self): { 'topic': 'topic', 'fcm_options': {'analytics_label': 'message-label'}, - 'android': {'fcm_options': {'analytics_label': 'android-label', - 'direct_boot_ok': False,}}, + 'android': {'fcm_options': {'analytics_label': 'android-label'}, + 'direct_boot_ok': False}, 'apns': {'fcm_options': {'analytics_label': 'apns-label', 'image': 'https://images.unsplash.com/photo-14944386399' '46-1ebd1d20bf85?fit=crop&w=900&q=60'}}, @@ -330,8 +331,7 @@ def test_invalid_analytics_label(self, data): def test_invalid_direct_boot_ok(self, data): with pytest.raises(ValueError): check_encoding(messaging.Message( - topic='topic', android=messaging.AndroidConfig( - fcm_options=messaging.AndroidFCMOptions(direct_boot_ok=data)))) + topic='topic', android=messaging.AndroidConfig(direct_boot_ok=data))) def test_android_config(self): @@ -343,7 +343,8 @@ def test_android_config(self): priority='high', ttl=123, data={'k1': 'v1', 'k2': 'v2'}, - fcm_options=messaging.AndroidFCMOptions('analytics_label_v1', True) + fcm_options=messaging.AndroidFCMOptions('analytics_label_v1'), + direct_boot_ok=True, ) ) expected = { @@ -359,8 +360,8 @@ def test_android_config(self): }, 'fcm_options': { 'analytics_label': 'analytics_label_v1', - 'direct_boot_ok': True, }, + 'direct_boot_ok': True, }, } check_encoding(msg, expected) From 7294ae3ca4c668968b4b308582d46ec4ff49a895 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Mon, 20 Nov 2023 13:11:28 -0500 Subject: [PATCH 4/8] Make ref.push() only create local node --- firebase_admin/_db_utils.py | 88 +++++++++++++++++++++++++++++++++++++ firebase_admin/db.py | 22 +++++----- integration/test_db.py | 21 ++++++++- tests/test_db.py | 44 ++++++++++++------- 4 files changed, 147 insertions(+), 28 deletions(-) create mode 100644 firebase_admin/_db_utils.py diff --git a/firebase_admin/_db_utils.py b/firebase_admin/_db_utils.py new file mode 100644 index 000000000..8e352238c --- /dev/null +++ b/firebase_admin/_db_utils.py @@ -0,0 +1,88 @@ +# Copyright 2023 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Internal utilities for Firebase Realtime Database module""" + +import time +import random +import math + +_PUSH_CHARS = '-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz' + +def time_now(): + return int(time.time()*1000) + +def _generate_next_push_id(): + """Creates a unique push id generator. + + Creates 20-character string identifiers with the following properties: + 1. They're based on timestamps so that they sort after any existing ids. + + 2. They contain 96-bits of random data after the timestamp so that IDs won't + collide with other clients' IDs. + + 3. They sort lexicographically*(so the timestamp is converted to characters + that will sort properly). + + 4. They're monotonically increasing. Even if you generate more than one in + the same timestamp, the latter ones will sort after the former ones. We do + this by using the previous random bits but "incrementing" them by 1 (only + in the case of a timestamp collision). + """ + + # Timestamp of last push, used to prevent local collisions if you push twice + # in one ms. + last_push_time = 0 + + # We generate 96-bits of randomness which get turned into 12 characters and + # appended to the timestamp to prevent collisions with other clients. We + # store the last characters we generated because in the event of a collision, + # we'll use those same characters except "incremented" by one. + last_rand_chars_indexes = [] + + def next_push_id(now): + nonlocal last_push_time + nonlocal last_rand_chars_indexes + is_duplicate_time = now == last_push_time + last_push_time = now + + push_id = '' + for _ in range(8): + push_id = _PUSH_CHARS[now % 64] + push_id + now = math.floor(now / 64) + + if not is_duplicate_time: + last_rand_chars_indexes = [] + for _ in range(12): + last_rand_chars_indexes.append(random.randrange(64)) + else: + for index in range(11, -1, -1): + if last_rand_chars_indexes[index] == 63: + last_rand_chars_indexes[index] = 0 + else: + break + if index != 0: + last_rand_chars_indexes[index] += 1 + elif index == 0 and last_rand_chars_indexes[index] != 0: + last_rand_chars_indexes[index] += 1 + + for index in range(12): + push_id += _PUSH_CHARS[last_rand_chars_indexes[index]] + + if len(push_id) != 20: + raise ValueError("push_id length should be 20") + return push_id + return next_push_id + +get_next_push_id = _generate_next_push_id() diff --git a/firebase_admin/db.py b/firebase_admin/db.py index 890968796..51b29116d 100644 --- a/firebase_admin/db.py +++ b/firebase_admin/db.py @@ -34,7 +34,7 @@ from firebase_admin import _http_client from firebase_admin import _sseclient from firebase_admin import _utils - +from firebase_admin import _db_utils _DB_ATTRIBUTE = '_database' _INVALID_PATH_CHARACTERS = '[].?#$' @@ -301,12 +301,13 @@ def set_if_unchanged(self, expected_etag, value): raise error - def push(self, value=''): + def push(self, value=None): """Creates a new child node. - The optional value argument can be used to provide an initial value for the child node. If - no value is provided, child node will have empty string as the default value. - + The optional value argument can be used to provide an initial value for the child node. + If you provide a value, a child node is created and the value written to that location. + If you don't provide a value, the child node is created but nothing is written to the + database and the child remains empty (but you can use the Reference elsewhere). Args: value: JSON-serializable initial value for the child node (optional). @@ -314,14 +315,15 @@ def push(self, value=''): Reference: A Reference representing the newly created child node. Raises: - ValueError: If the value is None. TypeError: If the value is not JSON-serializable. FirebaseError: If an error occurs while communicating with the remote database server. """ - if value is None: - raise ValueError('Value must not be None.') - output = self._client.body('post', self._add_suffix(), json=value) - push_id = output.get('name') + now = _db_utils.time_now() + push_id = _db_utils.get_next_push_id(now) + push_ref = self.child(push_id) + + if value is not None: + push_ref.set(value) return self.child(push_id) def update(self, value): diff --git a/integration/test_db.py b/integration/test_db.py index c448436d6..9fcca8bde 100644 --- a/integration/test_db.py +++ b/integration/test_db.py @@ -149,7 +149,7 @@ def test_push(self, testref): python = testref.parent ref = python.child('users').push() assert ref.path == '/_adminsdk/python/users/' + ref.key - assert ref.get() == '' + assert ref.get() is None def test_push_with_value(self, testref): python = testref.parent @@ -158,6 +158,25 @@ def test_push_with_value(self, testref): assert ref.path == '/_adminsdk/python/users/' + ref.key assert ref.get() == value + def test_push_to_local_ref(self, testref): + python = testref.parent + ref1 = python.child('games').push() + assert ref1.get() is None + ref2 = ref1.push("card") + assert ref2.parent.key == ref1.key + assert ref1.get() == {ref2.key: 'card'} + assert ref2.get() == 'card' + + def test_push_set_local_ref(self, testref): + python = testref.parent + ref1 = python.child('games').push().child('card') + ref2 = ref1.push() + assert ref2.get() is None + ref3 = ref1.push('heart') + ref2.set('spade') + assert ref2.get() == 'spade' + assert ref1.parent.get() == {'card': {ref2.key: 'spade', ref3.key: 'heart'}} + def test_set_primitive_value(self, testref): python = testref.parent ref = python.child('users').push() diff --git a/tests/test_db.py b/tests/test_db.py index aa2c83bd9..53a749d1d 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -19,6 +19,7 @@ import sys import time +from unittest import mock import pytest import firebase_admin @@ -145,7 +146,7 @@ def get(cls, ref): @classmethod def push(cls, ref): - ref.push() + ref.push({'foo': 'bar'}) @classmethod def set(cls, ref): @@ -392,33 +393,42 @@ def test_set_invalid_update(self, update): @pytest.mark.parametrize('data', valid_values) def test_push(self, data): ref = db.reference('/test') - recorder = self.instrument(ref, json.dumps({'name' : 'testkey'})) + recorder = self.instrument(ref, json.dumps({})) child = ref.push(data) assert isinstance(child, db.Reference) - assert child.key == 'testkey' + assert len(child.key) == 20 assert len(recorder) == 1 - assert recorder[0].method == 'POST' - assert recorder[0].url == 'https://test.firebaseio.com/test.json' + assert recorder[0].method == 'PUT' + assert recorder[0].url == f'https://test.firebaseio.com/test/{child.key}.json?print=silent' assert json.loads(recorder[0].body.decode()) == data assert recorder[0].headers['Authorization'] == 'Bearer mock-token' assert recorder[0].headers['User-Agent'] == db._USER_AGENT def test_push_default(self): ref = db.reference('/test') - recorder = self.instrument(ref, json.dumps({'name' : 'testkey'})) - assert ref.push().key == 'testkey' - assert len(recorder) == 1 - assert recorder[0].method == 'POST' - assert recorder[0].url == 'https://test.firebaseio.com/test.json' - assert json.loads(recorder[0].body.decode()) == '' - assert recorder[0].headers['Authorization'] == 'Bearer mock-token' - assert recorder[0].headers['User-Agent'] == db._USER_AGENT + recorder = self.instrument(ref, json.dumps({})) + child = ref.push() + assert isinstance(child, db.Reference) + assert len(child.key) == 20 + assert len(recorder) == 0 - def test_push_none_value(self): + @pytest.mark.parametrize('data', valid_values) + @mock.patch('time.time', mock.MagicMock(return_value=1700497750.2549)) + def test_push_duplicate_timestamp(self, data): ref = db.reference('/test') - self.instrument(ref, '') - with pytest.raises(ValueError): - ref.push(None) + recorder = self.instrument(ref, json.dumps({})) + child = [] + child.append(ref.push(data)) + child.append(ref.push(data)) + assert child[1].key > child[0].key + assert len(recorder) == 2 + for index, record in enumerate(recorder): + assert record.method == 'PUT' + assert record.url == \ + f'https://test.firebaseio.com/test/{child[index].key}.json?print=silent' + assert json.loads(record.body.decode()) == data + assert record.headers['Authorization'] == 'Bearer mock-token' + assert record.headers['User-Agent'] == db._USER_AGENT def test_delete(self): ref = db.reference('/test') From 580d2934ff6832e2f296302193c5296a643c342d Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Mon, 20 Nov 2023 14:33:39 -0500 Subject: [PATCH 5/8] Revert "fix: add to correct config." This reverts commit fb83427b5b1666c3fe20c14951291ee17ca74ec5. --- firebase_admin/_messaging_encoder.py | 4 ++-- firebase_admin/_messaging_utils.py | 10 +++++----- tests/test_messaging.py | 15 +++++++-------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/firebase_admin/_messaging_encoder.py b/firebase_admin/_messaging_encoder.py index 9910e5541..725db45cc 100644 --- a/firebase_admin/_messaging_encoder.py +++ b/firebase_admin/_messaging_encoder.py @@ -205,8 +205,6 @@ def encode_android(cls, android): 'AndroidConfig.restricted_package_name', android.restricted_package_name), 'ttl': cls.encode_ttl(android.ttl), 'fcm_options': cls.encode_android_fcm_options(android.fcm_options), - 'direct_boot_ok': _Validators.check_boolean( - 'AndroidFCMOptions.direct_boot_ok', android.direct_boot_ok), } result = cls.remove_null_values(result) priority = result.get('priority') @@ -225,6 +223,8 @@ def encode_android_fcm_options(cls, fcm_options): result = { 'analytics_label': _Validators.check_analytics_label( 'AndroidFCMOptions.analytics_label', fcm_options.analytics_label), + 'direct_boot_ok': _Validators.check_boolean( + 'AndroidFCMOptions.direct_boot_ok', fcm_options.direct_boot_ok) } result = cls.remove_null_values(result) return result diff --git a/firebase_admin/_messaging_utils.py b/firebase_admin/_messaging_utils.py index 29b8276bc..e10b869bd 100644 --- a/firebase_admin/_messaging_utils.py +++ b/firebase_admin/_messaging_utils.py @@ -49,12 +49,10 @@ class AndroidConfig: 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). - direct_boot_ok: A boolean indicating whether messages will be allowed to be delivered to - the app while the device is in direct boot mode (optional). """ def __init__(self, collapse_key=None, priority=None, ttl=None, restricted_package_name=None, - data=None, notification=None, fcm_options=None, direct_boot_ok=None): + data=None, notification=None, fcm_options=None): self.collapse_key = collapse_key self.priority = priority self.ttl = ttl @@ -62,7 +60,6 @@ def __init__(self, collapse_key=None, priority=None, ttl=None, restricted_packag self.data = data self.notification = notification self.fcm_options = fcm_options - self.direct_boot_ok = direct_boot_ok class AndroidNotification: @@ -206,10 +203,13 @@ class AndroidFCMOptions: Args: analytics_label: contains additional options for features provided by the FCM Android SDK (optional). + direct_boot_ok: A boolean indicating whether messages will be allowed to be delivered to + the app while the device is in direct boot mode. """ - def __init__(self, analytics_label=None): + def __init__(self, analytics_label=None, direct_boot_ok=None): self.analytics_label = analytics_label + self.direct_boot_ok = direct_boot_ok class WebpushConfig: diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 5072df6ea..895448e9e 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -250,8 +250,7 @@ def test_fcm_options(self): topic='topic', fcm_options=messaging.FCMOptions('message-label'), android=messaging.AndroidConfig( - fcm_options=messaging.AndroidFCMOptions('android-label'), - direct_boot_ok=False), + fcm_options=messaging.AndroidFCMOptions('android-label', False)), apns=messaging.APNSConfig(fcm_options= messaging.APNSFCMOptions( analytics_label='apns-label', @@ -261,8 +260,8 @@ def test_fcm_options(self): { 'topic': 'topic', 'fcm_options': {'analytics_label': 'message-label'}, - 'android': {'fcm_options': {'analytics_label': 'android-label'}, - 'direct_boot_ok': False}, + 'android': {'fcm_options': {'analytics_label': 'android-label', + 'direct_boot_ok': False,}}, 'apns': {'fcm_options': {'analytics_label': 'apns-label', 'image': 'https://images.unsplash.com/photo-14944386399' '46-1ebd1d20bf85?fit=crop&w=900&q=60'}}, @@ -331,7 +330,8 @@ def test_invalid_analytics_label(self, data): def test_invalid_direct_boot_ok(self, data): with pytest.raises(ValueError): check_encoding(messaging.Message( - topic='topic', android=messaging.AndroidConfig(direct_boot_ok=data))) + topic='topic', android=messaging.AndroidConfig( + fcm_options=messaging.AndroidFCMOptions(direct_boot_ok=data)))) def test_android_config(self): @@ -343,8 +343,7 @@ def test_android_config(self): priority='high', ttl=123, data={'k1': 'v1', 'k2': 'v2'}, - fcm_options=messaging.AndroidFCMOptions('analytics_label_v1'), - direct_boot_ok=True, + fcm_options=messaging.AndroidFCMOptions('analytics_label_v1', True) ) ) expected = { @@ -360,8 +359,8 @@ def test_android_config(self): }, 'fcm_options': { 'analytics_label': 'analytics_label_v1', + 'direct_boot_ok': True, }, - 'direct_boot_ok': True, }, } check_encoding(msg, expected) From 1a1b3f01792574af77f08a5c6b6fcf7c2a1ec52e Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Mon, 20 Nov 2023 14:33:56 -0500 Subject: [PATCH 6/8] Revert "Added tests." This reverts commit 9288589fcd931a4f53a413e037071f3d3e224b92. --- tests/test_messaging.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/tests/test_messaging.py b/tests/test_messaging.py index 895448e9e..71bb13eed 100644 --- a/tests/test_messaging.py +++ b/tests/test_messaging.py @@ -33,7 +33,6 @@ NON_OBJECT_ARGS = [list(), tuple(), dict(), 'foo', 0, 1, True, False] NON_LIST_ARGS = ['', tuple(), dict(), True, False, 1, 0, [1], ['foo', 1]] NON_UINT_ARGS = ['1.23s', list(), tuple(), dict(), -1.23] -NON_BOOL_ARGS = ['', list(), tuple(), dict(), 1, 0, [1], ['foo', 1], {1: 'foo'}, {'foo': 1}] HTTP_ERROR_CODES = { 400: exceptions.InvalidArgumentError, 403: exceptions.PermissionDeniedError, @@ -250,7 +249,7 @@ def test_fcm_options(self): topic='topic', fcm_options=messaging.FCMOptions('message-label'), android=messaging.AndroidConfig( - fcm_options=messaging.AndroidFCMOptions('android-label', False)), + fcm_options=messaging.AndroidFCMOptions('android-label')), apns=messaging.APNSConfig(fcm_options= messaging.APNSFCMOptions( analytics_label='apns-label', @@ -260,8 +259,7 @@ def test_fcm_options(self): { 'topic': 'topic', 'fcm_options': {'analytics_label': 'message-label'}, - 'android': {'fcm_options': {'analytics_label': 'android-label', - 'direct_boot_ok': False,}}, + 'android': {'fcm_options': {'analytics_label': 'android-label'}}, 'apns': {'fcm_options': {'analytics_label': 'apns-label', 'image': 'https://images.unsplash.com/photo-14944386399' '46-1ebd1d20bf85?fit=crop&w=900&q=60'}}, @@ -319,21 +317,6 @@ def test_invalid_data(self, data): check_encoding(messaging.Message( topic='topic', android=messaging.AndroidConfig(data=data))) - @pytest.mark.parametrize('data', NON_STRING_ARGS) - def test_invalid_analytics_label(self, data): - with pytest.raises(ValueError): - check_encoding(messaging.Message( - topic='topic', android=messaging.AndroidConfig( - fcm_options=messaging.AndroidFCMOptions(analytics_label=data)))) - - @pytest.mark.parametrize('data', NON_BOOL_ARGS) - def test_invalid_direct_boot_ok(self, data): - with pytest.raises(ValueError): - check_encoding(messaging.Message( - topic='topic', android=messaging.AndroidConfig( - fcm_options=messaging.AndroidFCMOptions(direct_boot_ok=data)))) - - def test_android_config(self): msg = messaging.Message( topic='topic', @@ -343,7 +326,7 @@ def test_android_config(self): priority='high', ttl=123, data={'k1': 'v1', 'k2': 'v2'}, - fcm_options=messaging.AndroidFCMOptions('analytics_label_v1', True) + fcm_options=messaging.AndroidFCMOptions('analytics_label_v1') ) ) expected = { @@ -359,7 +342,6 @@ def test_android_config(self): }, 'fcm_options': { 'analytics_label': 'analytics_label_v1', - 'direct_boot_ok': True, }, }, } From 724519474fc04f1e2ecce57f2f09766ce25a8a01 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Mon, 20 Nov 2023 14:34:06 -0500 Subject: [PATCH 7/8] Revert "feat(fcm): Enabled direct_boot_ok Android Config parameter." This reverts commit 6651302cb91f22e880aa544c1896879d3732c098. --- firebase_admin/_messaging_encoder.py | 11 ----------- firebase_admin/_messaging_utils.py | 5 +---- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/firebase_admin/_messaging_encoder.py b/firebase_admin/_messaging_encoder.py index 725db45cc..48a3dd3cd 100644 --- a/firebase_admin/_messaging_encoder.py +++ b/firebase_admin/_messaging_encoder.py @@ -160,15 +160,6 @@ def check_analytics_label(cls, label, value): raise ValueError('Malformed {}.'.format(label)) return value - @classmethod - def check_boolean(cls, label, value): - """Checks if the given value is boolean.""" - if value is None: - return None - if not isinstance(value, bool): - raise ValueError('{0} must be a boolean.'.format(label)) - return value - @classmethod def check_datetime(cls, label, value): """Checks if the given value is a datetime.""" @@ -223,8 +214,6 @@ def encode_android_fcm_options(cls, fcm_options): result = { 'analytics_label': _Validators.check_analytics_label( 'AndroidFCMOptions.analytics_label', fcm_options.analytics_label), - 'direct_boot_ok': _Validators.check_boolean( - 'AndroidFCMOptions.direct_boot_ok', fcm_options.direct_boot_ok) } result = cls.remove_null_values(result) return result diff --git a/firebase_admin/_messaging_utils.py b/firebase_admin/_messaging_utils.py index e10b869bd..64930f1b8 100644 --- a/firebase_admin/_messaging_utils.py +++ b/firebase_admin/_messaging_utils.py @@ -203,13 +203,10 @@ class AndroidFCMOptions: Args: analytics_label: contains additional options for features provided by the FCM Android SDK (optional). - direct_boot_ok: A boolean indicating whether messages will be allowed to be delivered to - the app while the device is in direct boot mode. """ - def __init__(self, analytics_label=None, direct_boot_ok=None): + def __init__(self, analytics_label=None): self.analytics_label = analytics_label - self.direct_boot_ok = direct_boot_ok class WebpushConfig: From 8dd4af3bcb2dad2ed834ddc90a2657f0623c4a11 Mon Sep 17 00:00:00 2001 From: Jonathan Edey Date: Mon, 20 Nov 2023 15:13:21 -0500 Subject: [PATCH 8/8] add extra check to unit test --- tests/test_db.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_db.py b/tests/test_db.py index 53a749d1d..11ed3cd6b 100644 --- a/tests/test_db.py +++ b/tests/test_db.py @@ -180,6 +180,8 @@ class TestReference: 500: exceptions.InternalError, } + duplicate_timestamp = time.time() + @classmethod def setup_class(cls): firebase_admin.initialize_app(testutils.MockCredential(), {'databaseURL' : cls.test_url}) @@ -413,14 +415,18 @@ def test_push_default(self): assert len(recorder) == 0 @pytest.mark.parametrize('data', valid_values) - @mock.patch('time.time', mock.MagicMock(return_value=1700497750.2549)) + @mock.patch('time.time', mock.MagicMock(return_value=duplicate_timestamp)) def test_push_duplicate_timestamp(self, data): ref = db.reference('/test') recorder = self.instrument(ref, json.dumps({})) child = [] child.append(ref.push(data)) child.append(ref.push(data)) - assert child[1].key > child[0].key + key1 = child[0].key + key2 = child[1].key + # First 8 digits are the encoded timestamp + assert key1[:8] == key2[:8] + assert key2 > key1 assert len(recorder) == 2 for index, record in enumerate(recorder): assert record.method == 'PUT'