From e498a9bf47b6240e893332fd305495805a49b4b2 Mon Sep 17 00:00:00 2001 From: Minjae Kim Date: Tue, 7 Apr 2026 13:57:29 +0900 Subject: [PATCH 1/2] Fix topic subscribe NoneType access --- fcm_django/models.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/fcm_django/models.py b/fcm_django/models.py index 67eeca68..d078bf87 100644 --- a/fcm_django/models.py +++ b/fcm_django/models.py @@ -570,10 +570,12 @@ def handle_topic_subscription( for i in range(0, len(registration_ids), MAX_DEVICES_PER_SUBSCRIBE_REQUEST): batch_ids = registration_ids[i : i + MAX_DEVICES_PER_SUBSCRIBE_REQUEST] responses.extend( - messaging.subscribe_to_topic - if should_subscribe - else messaging.unsubscribe_from_topic - )(batch_ids, topic, app=app, **more_subscribe_kwargs) + ( + messaging.subscribe_to_topic + if should_subscribe + else messaging.unsubscribe_from_topic + )(batch_ids, topic, app=app, **more_subscribe_kwargs) + ) return FirebaseResponseDict( response=messaging.BatchResponse(responses), From 1a9a2563fcfc49ed7d598edf32b65e074064ddef Mon Sep 17 00:00:00 2001 From: Mojca Date: Tue, 7 Apr 2026 08:29:45 +0200 Subject: [PATCH 2/2] Fix bulk topic subscription response handling --- fcm_django/models.py | 22 +++++++++++---------- tests/test_models.py | 46 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/fcm_django/models.py b/fcm_django/models.py index d078bf87..cf98d26d 100644 --- a/fcm_django/models.py +++ b/fcm_django/models.py @@ -566,22 +566,24 @@ def handle_topic_subscription( app = SETTINGS["DEFAULT_FIREBASE_APP"] if app is None else app if not registration_ids: return self.get_default_topic_response() - responses: list[messaging.SendResponse] = [] + topic_results: list[dict[str, str]] = [{} for _ in registration_ids] for i in range(0, len(registration_ids), MAX_DEVICES_PER_SUBSCRIBE_REQUEST): batch_ids = registration_ids[i : i + MAX_DEVICES_PER_SUBSCRIBE_REQUEST] - responses.extend( - ( - messaging.subscribe_to_topic - if should_subscribe - else messaging.unsubscribe_from_topic - )(batch_ids, topic, app=app, **more_subscribe_kwargs) - ) + batch_response = ( + messaging.subscribe_to_topic + if should_subscribe + else messaging.unsubscribe_from_topic + )(batch_ids, topic, app=app, **more_subscribe_kwargs) + for error in batch_response.errors: + topic_results[i + error.index] = {"error": error.reason} + + response = messaging.TopicManagementResponse({"results": topic_results}) return FirebaseResponseDict( - response=messaging.BatchResponse(responses), + response=response, registration_ids_sent=registration_ids, deactivated_registration_ids=self.deactivate_devices_with_error_results( - registration_ids, responses + registration_ids, response.errors ), ) diff --git a/tests/test_models.py b/tests/test_models.py index a7596733..71d26fda 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -97,6 +97,52 @@ def test_firebase_response_dict_summary_for_topic_response(mocker): assert result.failed_exceptions == ["messaging/mismatched-credential"] +@pytest.mark.django_db +def test_queryset_handle_topic_subscription_aggregates_topic_errors(mocker): + registration_ids = ["token-1", "token-2", "token-3"] + + mock_subscribe = mocker.patch("fcm_django.models.messaging.subscribe_to_topic") + mock_subscribe.side_effect = [ + mocker.Mock( + spec=["errors"], + errors=[mocker.Mock(index=1, reason="messaging/mismatched-credential")], + ), + mocker.Mock( + spec=["errors"], + errors=[ + mocker.Mock( + index=0, + reason="messaging/registration-token-not-registered", + ) + ], + ), + ] + mocker.patch("fcm_django.models.MAX_DEVICES_PER_SUBSCRIBE_REQUEST", 2) + + response = FCMDevice.objects.none().handle_topic_subscription( + True, + topic="topic-name", + skip_registration_id_lookup=True, + additional_registration_ids=registration_ids, + ) + + assert mock_subscribe.call_args_list == [ + mocker.call( + ["token-1", "token-2"], + "topic-name", + app=None, + ), + mocker.call( + ["token-3"], + "topic-name", + app=None, + ), + ] + assert response.failure_count == 2 + assert response.failed_registration_ids == ["token-2", "token-3"] + assert [error.index for error in response.response.errors] == [1, 2] + + @pytest.mark.django_db class TestFCMDeviceSendMessage: def assert_sent_successfully(