From 3345cd01041f9749b5385a3e8e345eb71440ab96 Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Fri, 5 Jun 2026 00:19:22 -0700 Subject: [PATCH 01/13] Add fid and deprecate token for Message and add fids and deprecate tokens for MulticastMessage --- .../FirebaseMessagingTest.cs | 2 + .../Messaging/FirebaseMessagingClientTest.cs | 24 ++++ .../Messaging/MessageTest.cs | 107 ++++++++++++++++++ .../Messaging/MulticastMessageTest.cs | 66 ++++++++++- .../FirebaseAdmin/Messaging/Message.cs | 19 +++- .../Messaging/MulticastMessage.cs | 80 ++++++++++--- 6 files changed, 274 insertions(+), 24 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs index 24292ba7..6ad77e28 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs @@ -120,11 +120,13 @@ public async Task SendEachForMulticast() TimeToLive = TimeSpan.FromHours(1), RestrictedPackageName = "com.google.firebase.testing", }, +#pragma warning disable CS0618 Tokens = new[] { "token1", "token2", }, +#pragma warning restore CS0618 }; var response = await FirebaseMessaging.DefaultInstance.SendEachForMulticastAsync(multicastMessage, dryRun: true); Assert.NotNull(response); diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs index 73c5c211..a533ec54 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs @@ -160,11 +160,15 @@ public async Task SendEachAsync() var client = this.CreateMessagingClient(factory); var message1 = new Message() { +#pragma warning disable CS0618 Token = "test-token1", +#pragma warning restore CS0618 }; var message2 = new Message() { +#pragma warning disable CS0618 Token = "test-token2", +#pragma warning restore CS0618 }; var response = await client.SendEachAsync(new[] { message1, message2 }); @@ -225,11 +229,15 @@ public async Task SendEachAsyncWithError() var client = this.CreateMessagingClient(factory); var message1 = new Message() { +#pragma warning disable CS0618 Token = "test-token1", +#pragma warning restore CS0618 }; var message2 = new Message() { +#pragma warning disable CS0618 Token = "test-token2", +#pragma warning restore CS0618 }; var response = await client.SendEachAsync(new[] { message1, message2 }); @@ -292,11 +300,15 @@ public async Task SendEachAsyncWithErrorNoDetail() var client = this.CreateMessagingClient(factory); var message1 = new Message() { +#pragma warning disable CS0618 Token = "test-token1", +#pragma warning restore CS0618 }; var message2 = new Message() { +#pragma warning disable CS0618 Token = "test-token2", +#pragma warning restore CS0618 }; var response = await client.SendEachAsync(new[] { message1, message2 }); @@ -389,11 +401,15 @@ public async Task SendAllAsync() var client = this.CreateMessagingClient(factory); var message1 = new Message() { +#pragma warning disable CS0618 Token = "test-token1", +#pragma warning restore CS0618 }; var message2 = new Message() { +#pragma warning disable CS0618 Token = "test-token2", +#pragma warning restore CS0618 }; var response = await client.SendAllAsync(new[] { message1, message2 }); @@ -467,11 +483,15 @@ public async Task SendAllAsyncWithError() var client = this.CreateMessagingClient(factory); var message1 = new Message() { +#pragma warning disable CS0618 Token = "test-token1", +#pragma warning restore CS0618 }; var message2 = new Message() { +#pragma warning disable CS0618 Token = "test-token2", +#pragma warning restore CS0618 }; var response = await client.SendAllAsync(new[] { message1, message2 }); @@ -546,11 +566,15 @@ public async Task SendAllAsyncWithErrorNoDetail() }); var message1 = new Message() { +#pragma warning disable CS0618 Token = "test-token1", +#pragma warning restore CS0618 }; var message2 = new Message() { +#pragma warning disable CS0618 Token = "test-token2", +#pragma warning restore CS0618 }; var response = await client.SendAllAsync(new[] { message1, message2 }); diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs index 3b21d78e..b496067d 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessageTest.cs @@ -26,9 +26,14 @@ public class MessageTest [Fact] public void EmptyMessage() { +#pragma warning disable CS0618 var message = new Message() { Token = "test-token" }; +#pragma warning restore CS0618 this.AssertJsonEquals(new JObject() { { "token", "test-token" } }, message); + message = new Message() { Fid = "test-fid" }; + this.AssertJsonEquals(new JObject() { { "fid", "test-fid" } }, message); + message = new Message() { Topic = "test-topic" }; this.AssertJsonEquals(new JObject() { { "topic", "test-topic" } }, message); @@ -147,6 +152,51 @@ public void MessageDeserialization() Assert.Equal(original.FcmOptions.AnalyticsLabel, copy.FcmOptions.AnalyticsLabel); } + [Fact] + public void MessageDeserializationWithFid() + { + var original = new Message() + { + Fid = "test-fid", + Data = new Dictionary() { { "key", "value" } }, + Notification = new Notification() + { + Title = "title", + Body = "body", + }, + Android = new AndroidConfig() + { + RestrictedPackageName = "test-pkg-name", + }, + Apns = new ApnsConfig() + { + Aps = new Aps() + { + AlertString = "test-alert", + }, + }, + Webpush = new WebpushConfig() + { + Data = new Dictionary() { { "key", "value" } }, + }, + FcmOptions = new FcmOptions() + { + AnalyticsLabel = "label", + }, + }; + var json = NewtonsoftJsonSerializer.Instance.Serialize(original); + var copy = NewtonsoftJsonSerializer.Instance.Deserialize(json); + Assert.Equal(original.Fid, copy.Fid); + Assert.Equal(original.Data, copy.Data); + Assert.Equal(original.Notification.Title, copy.Notification.Title); + Assert.Equal(original.Notification.Body, copy.Notification.Body); + Assert.Equal( + original.Android.RestrictedPackageName, copy.Android.RestrictedPackageName); + Assert.Equal(original.Apns.Aps.AlertString, copy.Apns.Aps.AlertString); + Assert.Equal(original.Webpush.Data, copy.Webpush.Data); + Assert.Equal(original.FcmOptions.AnalyticsLabel, copy.FcmOptions.AnalyticsLabel); + } + [Fact] public void MessageCopy() { @@ -168,6 +218,23 @@ public void MessageCopy() Assert.NotSame(original.Webpush, copy.Webpush); } + [Fact] + public void MessageWithFidOnly() + { + var original = new Message() + { + Fid = "test-fid", + Data = new Dictionary(), + Notification = new Notification(), + Android = new AndroidConfig(), + Apns = new ApnsConfig(), + Webpush = new WebpushConfig(), + }; + var copy = original.CopyAndValidate(); + Assert.NotSame(original, copy); + Assert.Equal("test-fid", copy.Fid); + } + [Fact] public void MessageWithoutTarget() { @@ -179,14 +246,18 @@ public void MultipleTargets() { var message = new Message() { +#pragma warning disable CS0618 Token = "test-token", +#pragma warning restore CS0618 Topic = "test-topic", }; Assert.Throws(() => message.CopyAndValidate()); message = new Message() { +#pragma warning disable CS0618 Token = "test-token", +#pragma warning restore CS0618 Condition = "test-condition", }; Assert.Throws(() => message.CopyAndValidate()); @@ -200,7 +271,43 @@ public void MultipleTargets() message = new Message() { +#pragma warning disable CS0618 + Token = "test-token", +#pragma warning restore CS0618 + Topic = "test-topic", + Condition = "test-condition", + }; + Assert.Throws(() => message.CopyAndValidate()); + + message = new Message() + { + Fid = "test-fid", +#pragma warning disable CS0618 + Token = "test-token", +#pragma warning restore CS0618 + }; + Assert.Throws(() => message.CopyAndValidate()); + + message = new Message() + { + Fid = "test-fid", + Topic = "test-topic", + }; + Assert.Throws(() => message.CopyAndValidate()); + + message = new Message() + { + Fid = "test-fid", + Condition = "test-condition", + }; + Assert.Throws(() => message.CopyAndValidate()); + + message = new Message() + { + Fid = "test-fid", +#pragma warning disable CS0618 Token = "test-token", +#pragma warning restore CS0618 Topic = "test-topic", Condition = "test-condition", }; diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MulticastMessageTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MulticastMessageTest.cs index 4430a394..d94b997e 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MulticastMessageTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MulticastMessageTest.cs @@ -1,4 +1,4 @@ -// Copyright 2018, Google Inc. All rights reserved. +// Copyright 2018, Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ namespace FirebaseAdmin.Tests.Messaging { public class MulticastMessageTest { +#pragma warning disable CS0618 [Fact] public void GetMessageList() { @@ -35,15 +36,50 @@ public void GetMessageList() Assert.Equal("test-token1", messages[0].Token); Assert.Equal("test-token2", messages[1].Token); } +#pragma warning restore CS0618 [Fact] - public void GetMessageListNoTokens() + public void GetMessageListFids() + { + var message = new MulticastMessage + { + Fids = new[] { "test-fid1", "test-fid2" }, + }; + + var messages = message.GetMessageList(); + + Assert.Equal(2, messages.Count); + Assert.Equal("test-fid1", messages[0].Fid); + Assert.Equal("test-fid2", messages[1].Fid); + } + + [Fact] + public void GetMessageListNoTargets() { var message = new MulticastMessage(); Assert.Throws(() => message.GetMessageList()); } +#pragma warning disable CS0618 + [Fact] + public void GetMessageListBothTargets() + { + var message = new MulticastMessage + { + Tokens = new[] { "test-token1" }, + Fids = new[] { "test-fid1" }, + }; + + var messages = message.GetMessageList(); + + Assert.Equal(2, messages.Count); + Assert.Equal("test-token1", messages[0].Token); + Assert.Equal("test-fid1", messages[1].Fid); + } +#pragma warning restore CS0618 + +#pragma warning disable CS0618 [Fact] public void GetMessageListTooManyTokens() { @@ -54,5 +90,31 @@ public void GetMessageListTooManyTokens() Assert.Throws(() => message.GetMessageList()); } +#pragma warning restore CS0618 + + [Fact] + public void GetMessageListTooManyFids() + { + var message = new MulticastMessage + { + Fids = Enumerable.Range(0, 501).Select(x => x.ToString()).ToList(), + }; + + Assert.Throws(() => message.GetMessageList()); + } + +#pragma warning disable CS0618 + [Fact] + public void GetMessageListTooManyCombinedTargets() + { + var message = new MulticastMessage + { + Tokens = Enumerable.Range(0, 250).Select(x => x.ToString()).ToList(), + Fids = Enumerable.Range(0, 251).Select(x => x.ToString()).ToList(), + }; + + Assert.Throws(() => message.GetMessageList()); + } +#pragma warning restore CS0618 } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 9a4d0502..dec96e1d 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -22,17 +22,25 @@ namespace FirebaseAdmin.Messaging /// /// Represents a message that can be sent via Firebase Cloud Messaging (FCM). Contains payload /// information as well as the recipient information. The recipient information must be - /// specified by setting exactly one of the , or - /// fields. + /// specified by setting exactly one of the , , + /// or fields. /// public sealed class Message { /// /// Gets or sets the registration token of the device to which the message should be sent. + /// Deprecated. Use instead. /// + [Obsolete("Deprecated. Use Fid instead.")] [JsonProperty("token")] public string Token { get; set; } + /// + /// Gets or sets the Firebase Installation ID (FID) of the device to which the message should be sent. + /// + [JsonProperty("fid")] + public string Fid { get; set; } + /// /// Gets or sets the name of the FCM topic to which the message should be sent. Topic names /// may contain the /topics/ prefix. @@ -116,10 +124,12 @@ private string UnprefixedTopic /// internal Message CopyAndValidate() { +#pragma warning disable CS0618 // Copy and validate the leaf-level properties var copy = new Message() { Token = this.Token, + Fid = this.Fid, Topic = this.Topic, Condition = this.Condition, Data = this.Data?.Copy(), @@ -127,13 +137,14 @@ internal Message CopyAndValidate() }; var list = new List() { - copy.Token, copy.Topic, copy.Condition, + copy.Token, copy.Fid, copy.Topic, copy.Condition, }; +#pragma warning restore CS0618 var targets = list.FindAll((target) => !string.IsNullOrEmpty(target)); if (targets.Count != 1) { throw new ArgumentException( - "Exactly one of Token, Topic or Condition is required."); + "Exactly one of Token, Fid, Topic or Condition is required."); } var topic = copy.UnprefixedTopic; diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs index df0a5f09..abc381ac 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs @@ -1,4 +1,4 @@ -// Copyright 2018, Google Inc. All rights reserved. +// Copyright 2018, Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,17 +19,23 @@ namespace FirebaseAdmin.Messaging { /// /// Represents a message that can be sent to multiple devices via Firebase Cloud Messaging (FCM). - /// Contains payload information as well as the list of device registration tokens to which the - /// message should be sent. A single MulticastMessage may contain up to 500 registration - /// tokens. + /// Contains payload information as well as the list of device registration tokens and/or + /// Firebase Installation IDs (FIDs) to which the message should be sent. A single + /// MulticastMessage may contain up to 500 tokens and FIDs combined. /// public sealed class MulticastMessage { /// /// Gets or sets the registration tokens for the devices to which the message should be distributed. /// + [Obsolete("Deprecated. Use Fids instead.")] public IReadOnlyList Tokens { get; set; } + /// + /// Gets or sets the installation IDs (FIDs) for the devices to which the message should be distributed. + /// + public IReadOnlyList Fids { get; set; } + /// /// Gets or sets a collection of key-value pairs that will be added to the message as data /// fields. Keys and the values must not be null. @@ -58,14 +64,27 @@ public sealed class MulticastMessage internal List GetMessageList() { +#pragma warning disable CS0618 var tokens = this.Tokens; +#pragma warning restore CS0618 + var fids = this.Fids; + + var tokensCopy = tokens != null ? new List(tokens) : null; + var fidsCopy = fids != null ? new List(fids) : null; - if (tokens == null || tokens.Count > 500) + var tokensCount = tokensCopy?.Count ?? 0; + var fidsCount = fidsCopy?.Count ?? 0; + var totalCount = tokensCount + fidsCount; + + if (totalCount == 0) { - throw new ArgumentException("Tokens must be non-null and contain at most 500 tokens."); + throw new ArgumentException("tokens and fids cannot be both null or empty"); } - var tokensCopy = new List(tokens); + if (totalCount > 500) + { + throw new ArgumentException("Total number of tokens and fids must not exceed 500."); + } var templateMessage = new Message { @@ -76,20 +95,45 @@ internal List GetMessageList() Webpush = this.Webpush?.CopyAndValidate(), }; - var messages = new List(tokensCopy.Count); + var messages = new List(totalCount); - foreach (var token in tokensCopy) + if (tokensCopy != null) { - var message = new Message + foreach (var token in tokensCopy) { - Android = templateMessage.Android, - Apns = templateMessage.Apns, - Data = templateMessage.Data, - Notification = templateMessage.Notification, - Webpush = templateMessage.Webpush, - Token = token, - }; - messages.Add(message); + var message = new Message + { + Android = templateMessage.Android, + Apns = templateMessage.Apns, + Data = templateMessage.Data, + Notification = templateMessage.Notification, + Webpush = templateMessage.Webpush, + }; + +#pragma warning disable CS0618 + message.Token = token; +#pragma warning restore CS0618 + + messages.Add(message); + } + } + + if (fidsCopy != null) + { + foreach (var fid in fidsCopy) + { + var message = new Message + { + Android = templateMessage.Android, + Apns = templateMessage.Apns, + Data = templateMessage.Data, + Notification = templateMessage.Notification, + Webpush = templateMessage.Webpush, + Fid = fid, + }; + + messages.Add(message); + } } return messages; From c210002ac92c2afa95131e4705a1c2af4313ecf0 Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:19:57 -0700 Subject: [PATCH 02/13] Add integration tests --- .../FirebaseMessagingTest.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs index 6ad77e28..fd697ee4 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs @@ -135,6 +135,35 @@ public async Task SendEachForMulticast() Assert.NotNull(response.Responses[1].Exception); } + [Fact] + public async Task SendEachForMulticastFids() + { + var multicastMessage = new MulticastMessage + { + Notification = new Notification() + { + Title = "Title", + Body = "Body", + }, + Android = new AndroidConfig() + { + Priority = Priority.Normal, + TimeToLive = TimeSpan.FromHours(1), + RestrictedPackageName = "com.google.firebase.testing", + }, + Fids = new[] + { + "fid1", + "fid2", + }, + }; + var response = await FirebaseMessaging.DefaultInstance.SendEachForMulticastAsync(multicastMessage, dryRun: true); + Assert.NotNull(response); + Assert.Equal(2, response.FailureCount); + Assert.Equal(MessagingErrorCode.Unregistered, response.Responses[0].Exception.MessagingErrorCode); + Assert.Equal(MessagingErrorCode.Unregistered, response.Responses[1].Exception.MessagingErrorCode); + } + [Fact] public async Task SubscribeToTopic() { From 1c437f4649a4d6e41fe98587ee839fddb0cf6062 Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:27:48 -0700 Subject: [PATCH 03/13] Update error code documentation --- FirebaseAdmin/FirebaseAdmin/Messaging/MessagingErrorCode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/MessagingErrorCode.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/MessagingErrorCode.cs index ff29ba94..9e7e47e8 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/MessagingErrorCode.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/MessagingErrorCode.cs @@ -50,7 +50,7 @@ public enum MessagingErrorCode Unavailable, /// - /// App instance was unregistered from FCM. This usually means that the token used is no + /// App instance was unregistered from FCM. This usually means that the token or FID used is no /// longer valid and a new one must be used. /// Unregistered, From 6bd78425cbdf0cca1210c92223416873633b11ca Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Fri, 5 Jun 2026 12:54:39 -0700 Subject: [PATCH 04/13] =?UTF-8?q?Add=20pragma=20warning=20disable=20CS0618?= =?UTF-8?q?=C2=A0wrappers=20in=20snippets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FirebaseExceptionSnippets.cs | 2 ++ .../FirebaseMessagingSnippets.cs | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseExceptionSnippets.cs b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseExceptionSnippets.cs index a3b1e66d..86e889bd 100644 --- a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseExceptionSnippets.cs +++ b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseExceptionSnippets.cs @@ -139,7 +139,9 @@ private static Message CreateNotification(string deviceToken) { return new Message() { +#pragma warning disable CS0618 Token = deviceToken, +#pragma warning restore CS0618 Notification = new Notification() { Title = "Test notification", diff --git a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseMessagingSnippets.cs b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseMessagingSnippets.cs index d4be9b5b..487b0266 100644 --- a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseMessagingSnippets.cs +++ b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseMessagingSnippets.cs @@ -35,7 +35,9 @@ internal static async Task SendToTokenAsync() { "score", "850" }, { "time", "2:45" }, }, +#pragma warning disable CS0618 Token = registrationToken, +#pragma warning restore CS0618 }; // Send a message to the device corresponding to the provided @@ -105,7 +107,9 @@ internal static async Task SendDryRunAsync() { "score", "850" }, { "time", "2:45" }, }, +#pragma warning disable CS0618 Token = "token", +#pragma warning restore CS0618 }; // [START send_dry_run] @@ -131,7 +135,9 @@ internal static async Task SendEachAsync() Title = "Price drop", Body = "5% off all electronics", }, +#pragma warning disable CS0618 Token = registrationToken, +#pragma warning restore CS0618 }, new Message() { @@ -164,7 +170,9 @@ internal static async Task SendEachForMulticastAsync() }; var message = new MulticastMessage() { +#pragma warning disable CS0618 Tokens = registrationTokens, +#pragma warning restore CS0618 Data = new Dictionary() { { "score", "850" }, @@ -191,7 +199,9 @@ internal static async Task SendEachForMulticastAndHandleErrorsAsync() }; var message = new MulticastMessage() { +#pragma warning disable CS0618 Tokens = registrationTokens, +#pragma warning restore CS0618 Data = new Dictionary() { { "score", "850" }, From 8c1f06d32fd082ec95b82c4cceec2d6bc0b7a36a Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Fri, 5 Jun 2026 13:29:18 -0700 Subject: [PATCH 05/13] Fix error message formatting --- FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs index abc381ac..aca93346 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs @@ -78,12 +78,12 @@ internal List GetMessageList() if (totalCount == 0) { - throw new ArgumentException("tokens and fids cannot be both null or empty"); + throw new ArgumentException("Tokens and Fids cannot be both null or empty."); } if (totalCount > 500) { - throw new ArgumentException("Total number of tokens and fids must not exceed 500."); + throw new ArgumentException("Total number of Tokens and Fids must not exceed 500."); } var templateMessage = new Message From 2044811bc9d421be2bdc1672f12401356916e06c Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:00:38 -0700 Subject: [PATCH 06/13] Update documentation comments for both SendEachForMulticastAsync and SendMulticastAsync --- .../Messaging/FirebaseMessaging.cs | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs index d26bb3a1..07137d77 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs @@ -250,7 +250,8 @@ public async Task SendEachAsync(IEnumerable messages, bo } /// - /// Sends the given multicast message to all the FCM registration tokens specified in it. + /// Sends the given multicast message to all the FCM registration tokens and FIDs specified in it. + /// The order of responses in the corresponds to the order of tokens, followed by the order of FIDs. /// Unlike , this method makes an /// HTTP call for each token in the given multicast message. /// @@ -266,7 +267,8 @@ public async Task SendEachForMulticastAsync(MulticastMessage mess } /// - /// Sends the given multicast message to all the FCM registration tokens specified in it. + /// Sends the given multicast message to all the FCM registration tokens and FIDs specified in it. + /// The order of responses in the corresponds to the order of tokens, followed by the order of FIDs. /// Unlike , this /// method makes an HTTP call for each token in the given multicast message. /// @@ -284,7 +286,8 @@ public async Task SendEachForMulticastAsync(MulticastMessage mess } /// - /// Sends the given multicast message to all the FCM registration tokens specified in it. + /// Sends the given multicast message to all the FCM registration tokens and FIDs specified in it. + /// The order of responses in the corresponds to the order of tokens, followed by the order of FIDs. /// Unlike , this method makes an /// HTTP call for each token in the given multicast message. /// If the option is set to true, the message will not be @@ -307,7 +310,8 @@ public async Task SendEachForMulticastAsync(MulticastMessage mess } /// - /// Sends the given multicast message to all the FCM registration tokens specified in it. + /// Sends the given multicast message to all the FCM registration tokens and FIDs specified in it. + /// The order of responses in the corresponds to the order of tokens, followed by the order of FIDs. /// Unlike , /// this method makes an HTTP call for each token in the given multicast message. /// If the option is set to true, the message will not be @@ -412,7 +416,8 @@ public async Task SendAllAsync(IEnumerable messages, boo } /// - /// Sends the given multicast message to all the FCM registration tokens specified in it. + /// Sends the given multicast message to all the FCM registration tokens and FIDs specified in it. + /// The order of responses in the corresponds to the order of tokens, followed by the order of FIDs. /// /// If an error occurs while sending the /// messages. @@ -427,7 +432,8 @@ public async Task SendMulticastAsync(MulticastMessage message) } /// - /// Sends the given multicast message to all the FCM registration tokens specified in it. + /// Sends the given multicast message to all the FCM registration tokens and FIDs specified in it. + /// The order of responses in the corresponds to the order of tokens, followed by the order of FIDs. /// /// If an error occurs while sending the /// messages. @@ -444,7 +450,8 @@ public async Task SendMulticastAsync(MulticastMessage message, Ca } /// - /// Sends the given multicast message to all the FCM registration tokens specified in it. + /// Sends the given multicast message to all the FCM registration tokens and FIDs specified in it. + /// The order of responses in the corresponds to the order of tokens, followed by the order of FIDs. /// If the option is set to true, the message will not be /// actually sent to the recipients. Instead, the FCM service performs all the necessary /// validations, and emulates the send operation. This is a good way to check if a @@ -466,7 +473,8 @@ public async Task SendMulticastAsync(MulticastMessage message, bo } /// - /// Sends the given multicast message to all the FCM registration tokens specified in it. + /// Sends the given multicast message to all the FCM registration tokens and FIDs specified in it. + /// The order of responses in the corresponds to the order of tokens, followed by the order of FIDs. /// If the option is set to true, the message will not be /// actually sent to the recipients. Instead, the FCM service performs all the necessary /// validations, and emulates the send operation. This is a good way to check if a From e11386356fb8f649482f9507161376f45f041c4b Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:36:27 -0700 Subject: [PATCH 07/13] Replace token payload with the fid payload in firebaseMessagingClientTest --- .../Messaging/FirebaseMessagingClientTest.cs | 102 +++++++----------- 1 file changed, 39 insertions(+), 63 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs index a533ec54..1e9c9f26 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs @@ -141,7 +141,7 @@ public async Task SendEachAsync() GenerateResponse = (incomingRequest) => { string name; - if (incomingRequest.Body.Contains("test-token1")) + if (incomingRequest.Body.Contains("test-fid1")) { name = "projects/fir-adminintegrationtests/messages/8580920590356323124"; } @@ -160,15 +160,11 @@ public async Task SendEachAsync() var client = this.CreateMessagingClient(factory); var message1 = new Message() { -#pragma warning disable CS0618 - Token = "test-token1", -#pragma warning restore CS0618 + Fid = "test-fid1", }; var message2 = new Message() { -#pragma warning disable CS0618 - Token = "test-token2", -#pragma warning restore CS0618 + Fid = "test-fid2", }; var response = await client.SendEachAsync(new[] { message1, message2 }); @@ -189,7 +185,7 @@ public async Task SendEachAsyncWithError() GenerateResponse = (incomingRequest) => { string name; - if (incomingRequest.Body.Contains("test-token1")) + if (incomingRequest.Body.Contains("test-fid1")) { name = "projects/fir-adminintegrationtests/messages/8580920590356323124"; return new FirebaseMessagingClient.SingleMessageResponse() @@ -201,8 +197,8 @@ public async Task SendEachAsyncWithError() { return @"{ ""error"": { - ""status"": ""INVALID_ARGUMENT"", - ""message"": ""The registration token is not a valid FCM registration token"", + ""status"": ""NOT_FOUND"", + ""message"": ""Requested entity was not found."", ""details"": [ { ""@type"": ""type.googleapis.com/google.firebase.fcm.v1.FcmError"", @@ -215,13 +211,13 @@ public async Task SendEachAsyncWithError() }, GenerateStatusCode = (incomingRequest) => { - if (incomingRequest.Body.Contains("test-token1")) + if (incomingRequest.Body.Contains("test-fid1")) { return HttpStatusCode.OK; } else { - return HttpStatusCode.InternalServerError; + return HttpStatusCode.NotFound; } }, }; @@ -229,15 +225,11 @@ public async Task SendEachAsyncWithError() var client = this.CreateMessagingClient(factory); var message1 = new Message() { -#pragma warning disable CS0618 - Token = "test-token1", -#pragma warning restore CS0618 + Fid = "test-fid1", }; var message2 = new Message() { -#pragma warning disable CS0618 - Token = "test-token2", -#pragma warning restore CS0618 + Fid = "test-fid2", }; var response = await client.SendEachAsync(new[] { message1, message2 }); @@ -248,8 +240,8 @@ public async Task SendEachAsyncWithError() var exception = response.Responses[1].Exception; Assert.NotNull(exception); - Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode); - Assert.Equal("The registration token is not a valid FCM registration token", exception.Message); + Assert.Equal(ErrorCode.NotFound, exception.ErrorCode); + Assert.Equal("Requested entity was not found.", exception.Message); Assert.Equal(MessagingErrorCode.Unregistered, exception.MessagingErrorCode); Assert.NotNull(exception.HttpResponse); @@ -266,7 +258,7 @@ public async Task SendEachAsyncWithErrorNoDetail() GenerateResponse = (incomingRequest) => { string name; - if (incomingRequest.Body.Contains("test-token1")) + if (incomingRequest.Body.Contains("test-fid1")) { name = "projects/fir-adminintegrationtests/messages/8580920590356323124"; return new FirebaseMessagingClient.SingleMessageResponse() @@ -278,21 +270,21 @@ public async Task SendEachAsyncWithErrorNoDetail() { return @"{ ""error"": { - ""status"": ""INVALID_ARGUMENT"", - ""message"": ""The registration token is not a valid FCM registration token"", + ""status"": ""NOT_FOUND"", + ""message"": ""Requested entity was not found."", } }"; } }, GenerateStatusCode = (incomingRequest) => { - if (incomingRequest.Body.Contains("test-token1")) + if (incomingRequest.Body.Contains("test-fid1")) { return HttpStatusCode.OK; } else { - return HttpStatusCode.InternalServerError; + return HttpStatusCode.NotFound; } }, }; @@ -300,15 +292,11 @@ public async Task SendEachAsyncWithErrorNoDetail() var client = this.CreateMessagingClient(factory); var message1 = new Message() { -#pragma warning disable CS0618 - Token = "test-token1", -#pragma warning restore CS0618 + Fid = "test-fid1", }; var message2 = new Message() { -#pragma warning disable CS0618 - Token = "test-token2", -#pragma warning restore CS0618 + Fid = "test-fid2", }; var response = await client.SendEachAsync(new[] { message1, message2 }); @@ -319,8 +307,8 @@ public async Task SendEachAsyncWithErrorNoDetail() var exception = response.Responses[1].Exception; Assert.NotNull(exception); - Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode); - Assert.Equal("The registration token is not a valid FCM registration token", exception.Message); + Assert.Equal(ErrorCode.NotFound, exception.ErrorCode); + Assert.Equal("Requested entity was not found.", exception.Message); Assert.Null(exception.MessagingErrorCode); Assert.NotNull(exception.HttpResponse); @@ -401,15 +389,11 @@ public async Task SendAllAsync() var client = this.CreateMessagingClient(factory); var message1 = new Message() { -#pragma warning disable CS0618 - Token = "test-token1", -#pragma warning restore CS0618 + Fid = "test-fid1", }; var message2 = new Message() { -#pragma warning disable CS0618 - Token = "test-token2", -#pragma warning restore CS0618 + Fid = "test-fid2", }; var response = await client.SendAllAsync(new[] { message1, message2 }); @@ -448,7 +432,7 @@ public async Task SendAllAsyncWithError() Content-Type: application/http Content-ID: response- -HTTP/1.1 400 Bad Request +HTTP/1.1 404 Not Found Content-Type: application/json; charset=UTF-8 Vary: Origin Vary: X-Origin @@ -456,15 +440,15 @@ public async Task SendAllAsyncWithError() { ""error"": { - ""code"": 400, - ""message"": ""The registration token is not a valid FCM registration token"", + ""code"": 404, + ""message"": ""Requested entity was not found."", ""details"": [ { ""@type"": ""type.googleapis.com/google.firebase.fcm.v1.FcmError"", ""errorCode"": ""UNREGISTERED"" } ], - ""status"": ""INVALID_ARGUMENT"" + ""status"": ""NOT_FOUND"" } } @@ -483,15 +467,11 @@ public async Task SendAllAsyncWithError() var client = this.CreateMessagingClient(factory); var message1 = new Message() { -#pragma warning disable CS0618 - Token = "test-token1", -#pragma warning restore CS0618 + Fid = "test-fid1", }; var message2 = new Message() { -#pragma warning disable CS0618 - Token = "test-token2", -#pragma warning restore CS0618 + Fid = "test-fid2", }; var response = await client.SendAllAsync(new[] { message1, message2 }); @@ -502,8 +482,8 @@ public async Task SendAllAsyncWithError() var exception = response.Responses[1].Exception; Assert.NotNull(exception); - Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode); - Assert.Equal("The registration token is not a valid FCM registration token", exception.Message); + Assert.Equal(ErrorCode.NotFound, exception.ErrorCode); + Assert.Equal("Requested entity was not found.", exception.Message); Assert.Equal(MessagingErrorCode.Unregistered, exception.MessagingErrorCode); Assert.NotNull(exception.HttpResponse); @@ -535,14 +515,14 @@ public async Task SendAllAsyncWithErrorNoDetail() Content-Type: application/http Content-ID: response- -HTTP/1.1 400 Bad Request +HTTP/1.1 404 Not Found Content-Type: application/json; charset=UTF-8 { ""error"": { - ""code"": 400, - ""message"": ""The registration token is not a valid FCM registration token"", - ""status"": ""INVALID_ARGUMENT"" + ""code"": 404, + ""message"": ""Requested entity was not found."", + ""status"": ""NOT_FOUND"" } } @@ -566,15 +546,11 @@ public async Task SendAllAsyncWithErrorNoDetail() }); var message1 = new Message() { -#pragma warning disable CS0618 - Token = "test-token1", -#pragma warning restore CS0618 + Fid = "test-fid1", }; var message2 = new Message() { -#pragma warning disable CS0618 - Token = "test-token2", -#pragma warning restore CS0618 + Fid = "test-fid2", }; var response = await client.SendAllAsync(new[] { message1, message2 }); @@ -585,8 +561,8 @@ public async Task SendAllAsyncWithErrorNoDetail() var exception = response.Responses[1].Exception; Assert.NotNull(exception); - Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode); - Assert.Equal("The registration token is not a valid FCM registration token", exception.Message); + Assert.Equal(ErrorCode.NotFound, exception.ErrorCode); + Assert.Equal("Requested entity was not found.", exception.Message); Assert.Null(exception.MessagingErrorCode); Assert.NotNull(exception.HttpResponse); From 6e85eec381ee63c0bcf03615fc0ebde97570928f Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Thu, 11 Jun 2026 13:36:16 -0700 Subject: [PATCH 08/13] Add code snippet to show the new usage with Fids --- .../FirebaseExceptionSnippets.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseExceptionSnippets.cs b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseExceptionSnippets.cs index 86e889bd..36f7a857 100644 --- a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseExceptionSnippets.cs +++ b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseExceptionSnippets.cs @@ -102,6 +102,40 @@ internal static async Task PlatformErrorCode(string deviceToken) // [END platform_error_code] } + internal static async Task PlatformErrorCodeWithFid(string fid) + { + // [START platform_error_code_fid] + var notification = CreateNotificationWithFid(fid); + try + { + await FirebaseMessaging.DefaultInstance.SendAsync(notification); + } + catch (FirebaseMessagingException ex) + { + // All exceptions contain a platform-level error code. Applications can inspect + // both the platform-level error code and any service-level error codes when + // implementing error handling logic. + if (ex.MessagingErrorCode == MessagingErrorCode.Unregistered) + { + // Service-level error code + Console.WriteLine("App instance has been unregistered"); + RemoveFidFromDatabase(fid); + } + else if (ex.ErrorCode == ErrorCode.Unavailable) + { + // Platform-level error code + Console.WriteLine("FCM service is temporarily unavailable"); + ScheduleForRetry(notification, TimeSpan.FromHours(1)); + } + else + { + Console.WriteLine($"Failed to send notification: {ex.Message}"); + } + } + + // [END platform_error_code_fid] + } + internal static async Task HttpResponse(string deviceToken) { // [START http_response] @@ -133,6 +167,37 @@ internal static async Task HttpResponse(string deviceToken) // [END http_response] } + internal static async Task HttpResponseWithFid(string fid) + { + // [START http_response_fid] + var notification = CreateNotificationWithFid(fid); + try + { + await FirebaseMessaging.DefaultInstance.SendAsync(notification); + } + catch (FirebaseMessagingException ex) + { + // If the exception was caused by a backend service error, applications can + // inspect the original error response received from the backend service to + // implement more advanced error handling behavior. + var response = ex.HttpResponse; + if (response != null) + { + Console.WriteLine($"FCM service responded with HTTP {response.StatusCode}"); + foreach (var entry in response.Headers) + { + Console.WriteLine($">>> {entry.Key}: {entry.Value}"); + } + + var body = await response.Content.ReadAsStringAsync(); + Console.WriteLine(">>>"); + Console.WriteLine($">>> {body}"); + } + } + + // [END http_response_fid] + } + private static void PerformPrivilegedOperation(string uid) { } private static Message CreateNotification(string deviceToken) @@ -149,8 +214,22 @@ private static Message CreateNotification(string deviceToken) }; } + private static Message CreateNotificationWithFid(string fid) + { + return new Message() + { + Fid = fid, + Notification = new Notification() + { + Title = "Test notification", + }, + }; + } + private static void RemoveTokenFromDatabase(string deviceToken) { } + private static void RemoveFidFromDatabase(string fid) { } + private static void ScheduleForRetry(Message message, TimeSpan waitTime) { } } } From a52c80db080f1444d92558206725bdcd82ed7af0 Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:08:01 -0700 Subject: [PATCH 09/13] Add token error tests back in FirebaseMessagingClientTest --- .../Messaging/FirebaseMessagingClientTest.cs | 318 ++++++++++++++++++ 1 file changed, 318 insertions(+) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs index 1e9c9f26..93bceb8b 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs @@ -249,6 +249,83 @@ public async Task SendEachAsyncWithError() this.CheckHeaders(handler.LastRequestHeaders); } + [Fact] + public async Task SendEachAsyncWithTokenError() + { + // Return a success for `message1` and an error for `message2` + var handler = new MockMessageHandler() + { + GenerateResponse = (incomingRequest) => + { + string name; + if (incomingRequest.Body.Contains("test-token1")) + { + name = "projects/fir-adminintegrationtests/messages/8580920590356323124"; + return new FirebaseMessagingClient.SingleMessageResponse() + { + Name = name, + }; + } + else + { + return @"{ + ""error"": { + ""status"": ""INVALID_ARGUMENT"", + ""message"": ""The registration token is not a valid FCM registration token"", + ""details"": [ + { + ""@type"": ""type.googleapis.com/google.firebase.fcm.v1.FcmError"", + ""errorCode"": ""UNREGISTERED"" + } + ] + } + }"; + } + }, + GenerateStatusCode = (incomingRequest) => + { + if (incomingRequest.Body.Contains("test-token1")) + { + return HttpStatusCode.OK; + } + else + { + return HttpStatusCode.BadRequest; + } + }, + }; + var factory = new MockHttpClientFactory(handler); + var client = this.CreateMessagingClient(factory); + var message1 = new Message() + { +#pragma warning disable CS0618 + Token = "test-token1", +#pragma warning restore CS0618, + }; + var message2 = new Message() + { +#pragma warning disable CS0618 + Token = "test-token2", +#pragma warning restore CS0618, + }; + + var response = await client.SendEachAsync(new[] { message1, message2 }); + + Assert.Equal(1, response.SuccessCount); + Assert.Equal(1, response.FailureCount); + Assert.Equal("projects/fir-adminintegrationtests/messages/8580920590356323124", response.Responses[0].MessageId); + + var exception = response.Responses[1].Exception; + Assert.NotNull(exception); + Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode); + Assert.Equal("The registration token is not a valid FCM registration token", exception.Message); + Assert.Equal(MessagingErrorCode.Unregistered, exception.MessagingErrorCode); + Assert.NotNull(exception.HttpResponse); + + Assert.Equal(2, handler.Calls); + this.CheckHeaders(handler.LastRequestHeaders); + } + [Fact] public async Task SendEachAsyncWithErrorNoDetail() { @@ -316,6 +393,77 @@ public async Task SendEachAsyncWithErrorNoDetail() this.CheckHeaders(handler.LastRequestHeaders); } + [Fact] + public async Task SendEachAsyncWithTokenErrorNoDetail() + { + // Return a success for `message1` and an error for `message2` + var handler = new MockMessageHandler() + { + GenerateResponse = (incomingRequest) => + { + string name; + if (incomingRequest.Body.Contains("test-token1")) + { + name = "projects/fir-adminintegrationtests/messages/8580920590356323124"; + return new FirebaseMessagingClient.SingleMessageResponse() + { + Name = name, + }; + } + else + { + return @"{ + ""error"": { + ""status"": ""INVALID_ARGUMENT"", + ""message"": ""The registration token is not a valid FCM registration token"", + } + }"; + } + }, + GenerateStatusCode = (incomingRequest) => + { + if (incomingRequest.Body.Contains("test-token1")) + { + return HttpStatusCode.OK; + } + else + { + return HttpStatusCode.BadRequest; + } + }, + }; + var factory = new MockHttpClientFactory(handler); + var client = this.CreateMessagingClient(factory); + var message1 = new Message() + { +#pragma warning disable CS0618 + Token = "test-token1", +#pragma warning restore CS0618, + }; + var message2 = new Message() + { +#pragma warning disable CS0618 + Token = "test-token2", +#pragma warning restore CS0618, + }; + + var response = await client.SendEachAsync(new[] { message1, message2 }); + + Assert.Equal(1, response.SuccessCount); + Assert.Equal(1, response.FailureCount); + Assert.Equal("projects/fir-adminintegrationtests/messages/8580920590356323124", response.Responses[0].MessageId); + + var exception = response.Responses[1].Exception; + Assert.NotNull(exception); + Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode); + Assert.Equal("The registration token is not a valid FCM registration token", exception.Message); + Assert.Null(exception.MessagingErrorCode); + Assert.NotNull(exception.HttpResponse); + + Assert.Equal(2, handler.Calls); + this.CheckHeaders(handler.LastRequestHeaders); + } + [Fact] public async Task SendEachAsyncNullList() { @@ -493,6 +641,93 @@ public async Task SendAllAsyncWithError() Assert.Equal(2, this.CountLinesWithPrefix(handler.LastRequestBody, $"X-Goog-Api-Client: {HttpUtils.GetMetricsHeader()}")); } + [Fact] + public async Task SendAllAsyncWithTokenError() + { + var rawResponse = @" +--batch_test-boundary +Content-Type: application/http +Content-ID: response- + +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Vary: Origin +Vary: X-Origin +Vary: Referer + +{ + ""name"": ""projects/fir-adminintegrationtests/messages/8580920590356323124"" +} + +--batch_test-boundary +Content-Type: application/http +Content-ID: response- + +HTTP/1.1 400 Bad Request +Content-Type: application/json; charset=UTF-8 +Vary: Origin +Vary: X-Origin +Vary: Referer + +{ + ""error"": { + ""code"": 404, + ""message"": ""The registration token is not a valid FCM registration token"", + ""details"": [ + { + ""@type"": ""type.googleapis.com/google.firebase.fcm.v1.FcmError"", + ""errorCode"": ""UNREGISTERED"" + } + ], + ""status"": ""INVALID_ARGUMENT"" + } +} + +--batch_test-boundary +"; + var handler = new MockMessageHandler() + { + Response = rawResponse, + ApplyHeaders = (_, headers) => + { + headers.Remove("Content-Type"); + headers.TryAddWithoutValidation("Content-Type", "multipart/mixed; boundary=batch_test-boundary"); + }, + }; + var factory = new MockHttpClientFactory(handler); + var client = this.CreateMessagingClient(factory); + var message1 = new Message() + { +#pragma warning disable CS0618 + Token = "test-token1", +#pragma warning restore CS0618, + }; + var message2 = new Message() + { +#pragma warning disable CS0618 + Token = "test-token2", +#pragma warning restore CS0618, + }; + + var response = await client.SendAllAsync(new[] { message1, message2 }); + + Assert.Equal(1, response.SuccessCount); + Assert.Equal(1, response.FailureCount); + Assert.Equal("projects/fir-adminintegrationtests/messages/8580920590356323124", response.Responses[0].MessageId); + + var exception = response.Responses[1].Exception; + Assert.NotNull(exception); + Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode); + Assert.Equal("The registration token is not a valid FCM registration token", exception.Message); + Assert.Equal(MessagingErrorCode.Unregistered, exception.MessagingErrorCode); + Assert.NotNull(exception.HttpResponse); + + Assert.Equal(1, handler.Calls); + Assert.Equal(2, this.CountLinesWithPrefix(handler.LastRequestBody, VersionHeader)); + Assert.Equal(2, this.CountLinesWithPrefix(handler.LastRequestBody, ApiFormatHeader)); + Assert.Equal(2, this.CountLinesWithPrefix(handler.LastRequestBody, $"X-Goog-Api-Client: {HttpUtils.GetMetricsHeader()}")); + } + [Fact] public async Task SendAllAsyncWithErrorNoDetail() { @@ -572,6 +807,89 @@ public async Task SendAllAsyncWithErrorNoDetail() Assert.Equal(2, this.CountLinesWithPrefix(handler.LastRequestBody, $"X-Goog-Api-Client: {HttpUtils.GetMetricsHeader()}")); } + [Fact] + public async Task SendAllAsyncWithTokenErrorNoDetail() + { + var rawResponse = @" +--batch_test-boundary +Content-Type: application/http +Content-ID: response- + +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Vary: Origin +Vary: X-Origin +Vary: Referer + +{ + ""name"": ""projects/fir-adminintegrationtests/messages/8580920590356323124"" +} + +--batch_test-boundary +Content-Type: application/http +Content-ID: response- + +HTTP/1.1 400 Bad Request +Content-Type: application/json; charset=UTF-8 + +{ + ""error"": { + ""code"": 404, + ""message"": ""The registration token is not a valid FCM registration token"", + ""status"": ""INVALID_ARGUMENT"" + } +} + +--batch_test-boundary +"; + var handler = new MockMessageHandler() + { + Response = rawResponse, + ApplyHeaders = (_, headers) => + { + headers.Remove("Content-Type"); + headers.TryAddWithoutValidation("Content-Type", "multipart/mixed; boundary=batch_test-boundary"); + }, + }; + var factory = new MockHttpClientFactory(handler); + var client = new FirebaseMessagingClient(new FirebaseMessagingClient.Args() + { + ClientFactory = factory, + Credential = MockCredential, + ProjectId = "test-project", + }); + var message1 = new Message() + { +#pragma warning disable CS0618 + Token = "test-token1", +#pragma warning restore CS0618, + }; + var message2 = new Message() + { +#pragma warning disable CS0618 + Token = "test-token2", +#pragma warning restore CS0618, + }; + + var response = await client.SendAllAsync(new[] { message1, message2 }); + + Assert.Equal(1, response.SuccessCount); + Assert.Equal(1, response.FailureCount); + Assert.Equal("projects/fir-adminintegrationtests/messages/8580920590356323124", response.Responses[0].MessageId); + + var exception = response.Responses[1].Exception; + Assert.NotNull(exception); + Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode); + Assert.Equal("The registration token is not a valid FCM registration token", exception.Message); + Assert.Null(exception.MessagingErrorCode); + Assert.NotNull(exception.HttpResponse); + + Assert.Equal(1, handler.Calls); + Assert.Equal(2, this.CountLinesWithPrefix(handler.LastRequestBody, VersionHeader)); + Assert.Equal(2, this.CountLinesWithPrefix(handler.LastRequestBody, ApiFormatHeader)); + Assert.Equal(2, this.CountLinesWithPrefix(handler.LastRequestBody, $"X-Goog-Api-Client: {HttpUtils.GetMetricsHeader()}")); + } + [Fact] public async Task SendAllAsyncNullList() { From 1eccd7467b81b71066e1c69bb35d1691dc80992a Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:19:15 -0700 Subject: [PATCH 10/13] Remove trailing comma for pragma warning disable CS0618 --- .../Messaging/FirebaseMessagingClientTest.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs index 93bceb8b..f04f6ee3 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs @@ -300,13 +300,13 @@ public async Task SendEachAsyncWithTokenError() { #pragma warning disable CS0618 Token = "test-token1", -#pragma warning restore CS0618, +#pragma warning restore CS0618 }; var message2 = new Message() { #pragma warning disable CS0618 Token = "test-token2", -#pragma warning restore CS0618, +#pragma warning restore CS0618 }; var response = await client.SendEachAsync(new[] { message1, message2 }); @@ -438,13 +438,13 @@ public async Task SendEachAsyncWithTokenErrorNoDetail() { #pragma warning disable CS0618 Token = "test-token1", -#pragma warning restore CS0618, +#pragma warning restore CS0618 }; var message2 = new Message() { #pragma warning disable CS0618 Token = "test-token2", -#pragma warning restore CS0618, +#pragma warning restore CS0618 }; var response = await client.SendEachAsync(new[] { message1, message2 }); @@ -700,13 +700,13 @@ public async Task SendAllAsyncWithTokenError() { #pragma warning disable CS0618 Token = "test-token1", -#pragma warning restore CS0618, +#pragma warning restore CS0618 }; var message2 = new Message() { #pragma warning disable CS0618 Token = "test-token2", -#pragma warning restore CS0618, +#pragma warning restore CS0618 }; var response = await client.SendAllAsync(new[] { message1, message2 }); @@ -862,13 +862,13 @@ public async Task SendAllAsyncWithTokenErrorNoDetail() { #pragma warning disable CS0618 Token = "test-token1", -#pragma warning restore CS0618, +#pragma warning restore CS0618 }; var message2 = new Message() { #pragma warning disable CS0618 Token = "test-token2", -#pragma warning restore CS0618, +#pragma warning restore CS0618 }; var response = await client.SendAllAsync(new[] { message1, message2 }); From 3e29acf220f893d7f2f0acc66916d3902d276fad Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:38:17 -0700 Subject: [PATCH 11/13] Update Fid to all capital Co-authored-by: OrlandriaC-G <112568492+OrlandriaH-G@users.noreply.github.com> --- FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index dec96e1d..29d7b315 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs @@ -144,7 +144,7 @@ internal Message CopyAndValidate() if (targets.Count != 1) { throw new ArgumentException( - "Exactly one of Token, Fid, Topic or Condition is required."); + "Exactly one of Token, FID, Topic or Condition is required."); } var topic = copy.UnprefixedTopic; From 634a7477304b4c84c4bc5975ef98f79594a0da08 Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:42:30 -0700 Subject: [PATCH 12/13] Update Fids to be all capital Co-authored-by: OrlandriaC-G <112568492+OrlandriaH-G@users.noreply.github.com> --- FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs index aca93346..c1beb051 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs @@ -78,7 +78,7 @@ internal List GetMessageList() if (totalCount == 0) { - throw new ArgumentException("Tokens and Fids cannot be both null or empty."); + throw new ArgumentException("Tokens and FIDs cannot be both null or empty."); } if (totalCount > 500) From bb66a4edd0a111d0205d8620e9f27178248986a9 Mon Sep 17 00:00:00 2001 From: Yvonne Pan <103622026+yvonnep165@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:43:03 -0700 Subject: [PATCH 13/13] Update Fids to FIDs Co-authored-by: OrlandriaC-G <112568492+OrlandriaH-G@users.noreply.github.com> --- FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs index c1beb051..62b506ef 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs @@ -83,7 +83,7 @@ internal List GetMessageList() if (totalCount > 500) { - throw new ArgumentException("Total number of Tokens and Fids must not exceed 500."); + throw new ArgumentException("Total number of Tokens and FIDs must not exceed 500."); } var templateMessage = new Message