diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs index 24292ba7..fd697ee4 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); @@ -133,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() { diff --git a/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseExceptionSnippets.cs b/FirebaseAdmin/FirebaseAdmin.Snippets/FirebaseExceptionSnippets.cs index a3b1e66d..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,13 +167,58 @@ 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) { return new Message() { +#pragma warning disable CS0618 Token = deviceToken, +#pragma warning restore CS0618 + Notification = new Notification() + { + Title = "Test notification", + }, + }; + } + + private static Message CreateNotificationWithFid(string fid) + { + return new Message() + { + Fid = fid, Notification = new Notification() { Title = "Test notification", @@ -149,6 +228,8 @@ private static Message CreateNotification(string deviceToken) private static void RemoveTokenFromDatabase(string deviceToken) { } + private static void RemoveFidFromDatabase(string fid) { } + private static void ScheduleForRetry(Message message, TimeSpan waitTime) { } } } 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" }, diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs index 73c5c211..f04f6ee3 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,11 +160,11 @@ public async Task SendEachAsync() var client = this.CreateMessagingClient(factory); var message1 = new Message() { - Token = "test-token1", + Fid = "test-fid1", }; var message2 = new Message() { - Token = "test-token2", + Fid = "test-fid2", }; var response = await client.SendEachAsync(new[] { message1, message2 }); @@ -178,6 +178,79 @@ public async Task SendEachAsync() [Fact] public async Task SendEachAsyncWithError() + { + // Return a success for `message1` and an error for `message2` + var handler = new MockMessageHandler() + { + GenerateResponse = (incomingRequest) => + { + string name; + if (incomingRequest.Body.Contains("test-fid1")) + { + name = "projects/fir-adminintegrationtests/messages/8580920590356323124"; + return new FirebaseMessagingClient.SingleMessageResponse() + { + Name = name, + }; + } + else + { + return @"{ + ""error"": { + ""status"": ""NOT_FOUND"", + ""message"": ""Requested entity was not found."", + ""details"": [ + { + ""@type"": ""type.googleapis.com/google.firebase.fcm.v1.FcmError"", + ""errorCode"": ""UNREGISTERED"" + } + ] + } + }"; + } + }, + GenerateStatusCode = (incomingRequest) => + { + if (incomingRequest.Body.Contains("test-fid1")) + { + return HttpStatusCode.OK; + } + else + { + return HttpStatusCode.NotFound; + } + }, + }; + var factory = new MockHttpClientFactory(handler); + var client = this.CreateMessagingClient(factory); + var message1 = new Message() + { + Fid = "test-fid1", + }; + var message2 = new Message() + { + Fid = "test-fid2", + }; + + 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.NotFound, exception.ErrorCode); + Assert.Equal("Requested entity was not found.", 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 SendEachAsyncWithTokenError() { // Return a success for `message1` and an error for `message2` var handler = new MockMessageHandler() @@ -217,7 +290,7 @@ public async Task SendEachAsyncWithError() } else { - return HttpStatusCode.InternalServerError; + return HttpStatusCode.BadRequest; } }, }; @@ -225,11 +298,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 }); @@ -251,6 +328,73 @@ public async Task SendEachAsyncWithError() [Fact] public async Task SendEachAsyncWithErrorNoDetail() + { + // Return a success for `message1` and an error for `message2` + var handler = new MockMessageHandler() + { + GenerateResponse = (incomingRequest) => + { + string name; + if (incomingRequest.Body.Contains("test-fid1")) + { + name = "projects/fir-adminintegrationtests/messages/8580920590356323124"; + return new FirebaseMessagingClient.SingleMessageResponse() + { + Name = name, + }; + } + else + { + return @"{ + ""error"": { + ""status"": ""NOT_FOUND"", + ""message"": ""Requested entity was not found."", + } + }"; + } + }, + GenerateStatusCode = (incomingRequest) => + { + if (incomingRequest.Body.Contains("test-fid1")) + { + return HttpStatusCode.OK; + } + else + { + return HttpStatusCode.NotFound; + } + }, + }; + var factory = new MockHttpClientFactory(handler); + var client = this.CreateMessagingClient(factory); + var message1 = new Message() + { + Fid = "test-fid1", + }; + var message2 = new Message() + { + Fid = "test-fid2", + }; + + 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.NotFound, exception.ErrorCode); + Assert.Equal("Requested entity was not found.", exception.Message); + Assert.Null(exception.MessagingErrorCode); + Assert.NotNull(exception.HttpResponse); + + Assert.Equal(2, handler.Calls); + this.CheckHeaders(handler.LastRequestHeaders); + } + + [Fact] + public async Task SendEachAsyncWithTokenErrorNoDetail() { // Return a success for `message1` and an error for `message2` var handler = new MockMessageHandler() @@ -284,7 +428,7 @@ public async Task SendEachAsyncWithErrorNoDetail() } else { - return HttpStatusCode.InternalServerError; + return HttpStatusCode.BadRequest; } }, }; @@ -292,11 +436,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 +537,11 @@ public async Task SendAllAsync() var client = this.CreateMessagingClient(factory); var message1 = new Message() { - Token = "test-token1", + Fid = "test-fid1", }; var message2 = new Message() { - Token = "test-token2", + Fid = "test-fid2", }; var response = await client.SendAllAsync(new[] { message1, message2 }); @@ -432,6 +580,89 @@ public async Task SendAllAsyncWithError() Content-Type: application/http Content-ID: response- +HTTP/1.1 404 Not Found +Content-Type: application/json; charset=UTF-8 +Vary: Origin +Vary: X-Origin +Vary: Referer + +{ + ""error"": { + ""code"": 404, + ""message"": ""Requested entity was not found."", + ""details"": [ + { + ""@type"": ""type.googleapis.com/google.firebase.fcm.v1.FcmError"", + ""errorCode"": ""UNREGISTERED"" + } + ], + ""status"": ""NOT_FOUND"" + } +} + +--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() + { + Fid = "test-fid1", + }; + var message2 = new Message() + { + Fid = "test-fid2", + }; + + 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.NotFound, exception.ErrorCode); + Assert.Equal("Requested entity was not found.", 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 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 @@ -440,7 +671,7 @@ public async Task SendAllAsyncWithError() { ""error"": { - ""code"": 400, + ""code"": 404, ""message"": ""The registration token is not a valid FCM registration token"", ""details"": [ { @@ -467,11 +698,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 }); @@ -515,12 +750,91 @@ public async Task SendAllAsyncWithErrorNoDetail() Content-Type: application/http Content-ID: response- +HTTP/1.1 404 Not Found +Content-Type: application/json; charset=UTF-8 + +{ + ""error"": { + ""code"": 404, + ""message"": ""Requested entity was not found."", + ""status"": ""NOT_FOUND"" + } +} + +--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() + { + Fid = "test-fid1", + }; + var message2 = new Message() + { + Fid = "test-fid2", + }; + + 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.NotFound, exception.ErrorCode); + Assert.Equal("Requested entity was not found.", 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 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"": 400, + ""code"": 404, ""message"": ""The registration token is not a valid FCM registration token"", ""status"": ""INVALID_ARGUMENT"" } @@ -546,11 +860,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/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 diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/Message.cs index 9a4d0502..29d7b315 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/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, diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/MulticastMessage.cs index df0a5f09..62b506ef 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;