diff --git a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs index 4a06d7d9..7a6839c4 100644 --- a/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using System.Collections.Generic; using System.Text.RegularExpressions; using System.Threading.Tasks; using FirebaseAdmin.Messaging; @@ -120,5 +121,29 @@ public async Task SendMulticast() Assert.NotNull(response.Responses[0].Exception); Assert.NotNull(response.Responses[1].Exception); } + + [Fact] + public async Task SubscribeToTopic() + { + var response = await FirebaseMessaging.DefaultInstance.SubscribeToTopicAsync("test-topic", new List { "token1", "token2" }); + Assert.NotNull(response); + Assert.Equal(2, response.FailureCount); + Assert.Equal("invalid-argument", response.Errors[0].Reason); + Assert.Equal(0, response.Errors[0].Index); + Assert.Equal("invalid-argument", response.Errors[1].Reason); + Assert.Equal(1, response.Errors[1].Index); + } + + [Fact] + public async Task UnsubscribeFromTopic() + { + var response = await FirebaseMessaging.DefaultInstance.UnsubscribeFromTopicAsync("test-topic", new List { "token1", "token2" }); + Assert.NotNull(response); + Assert.Equal(2, response.FailureCount); + Assert.Equal("invalid-argument", response.Errors[0].Reason); + Assert.Equal(0, response.Errors[0].Index); + Assert.Equal("invalid-argument", response.Errors[1].Reason); + Assert.Equal(1, response.Errors[1].Index); + } } } diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs index 5fef266a..aa37cf2f 100644 --- a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingTest.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using FirebaseAdmin.Tests; @@ -118,6 +119,56 @@ await Assert.ThrowsAsync( new Message() { Topic = "test-topic" }, canceller.Token)); } + [Fact] + public async Task SubscribeWithClientFactory() + { + var handler = new MockMessageHandler() + { + Response = @"{""results"":[{}]}", + }; + var factory = new MockHttpClientFactory(handler); + + var app = FirebaseApp.Create(new AppOptions() + { + Credential = GoogleCredential.FromAccessToken("test-token"), + HttpClientFactory = factory, + ProjectId = "test-project", + }); + FirebaseMessaging messaging = FirebaseMessaging.GetMessaging(app); + Assert.NotNull(messaging); + Assert.Same(messaging, FirebaseMessaging.GetMessaging(app)); + + var response = await messaging.SubscribeToTopicAsync("test-topic", new List { "test-token" }); + Assert.Equal(0, response.FailureCount); + Assert.Equal(1, response.SuccessCount); + app.Delete(); + } + + [Fact] + public async Task UnsubscribeWithClientFactory() + { + var handler = new MockMessageHandler() + { + Response = @"{""results"":[{}]}", + }; + var factory = new MockHttpClientFactory(handler); + + var app = FirebaseApp.Create(new AppOptions() + { + Credential = GoogleCredential.FromAccessToken("test-token"), + HttpClientFactory = factory, + ProjectId = "test-project", + }); + FirebaseMessaging messaging = FirebaseMessaging.GetMessaging(app); + Assert.NotNull(messaging); + Assert.Same(messaging, FirebaseMessaging.GetMessaging(app)); + + var response = await messaging.UnsubscribeFromTopicAsync("test-topic", new List { "test-token" }); + Assert.Equal(0, response.FailureCount); + Assert.Equal(1, response.SuccessCount); + app.Delete(); + } + public void Dispose() { FirebaseApp.DeleteAll(); diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/InstanceIdClientTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/InstanceIdClientTest.cs new file mode 100644 index 00000000..860fbc32 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/InstanceIdClientTest.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using FirebaseAdmin.Messaging; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Http; +using Xunit; + +namespace FirebaseAdmin.Tests.Messaging +{ + public class InstanceIdClientTest + { + private static readonly GoogleCredential MockCredential = + GoogleCredential.FromAccessToken("test-token"); + + [Fact] + public void NoCredential() + { + var clientFactory = new HttpClientFactory(); + Assert.Throws( + () => new InstanceIdClient(clientFactory, null)); + } + + [Fact] + public void NoClientFactory() + { + var clientFactory = new HttpClientFactory(); + Assert.Throws( + () => new InstanceIdClient(null, MockCredential)); + } + + [Fact] + public async Task SubscribeToTopicAsync() + { + var handler = new MockMessageHandler() + { + Response = @"{""results"":[{}]}", + }; + var factory = new MockHttpClientFactory(handler); + + var client = new InstanceIdClient(factory, MockCredential); + + var result = await client.SubscribeToTopicAsync("test-topic", new List { "abc123" }); + + Assert.Equal(1, result.SuccessCount); + } + + [Fact] + public async Task UnsubscribeFromTopicAsync() + { + var handler = new MockMessageHandler() + { + Response = @"{""results"":[{}]}", + }; + var factory = new MockHttpClientFactory(handler); + + var client = new InstanceIdClient(factory, MockCredential); + + var result = await client.UnsubscribeFromTopicAsync("test-topic", new List { "abc123" }); + + Assert.Equal(1, result.SuccessCount); + } + + [Fact] + public async Task BadRequest() + { + var handler = new MockMessageHandler() + { + StatusCode = HttpStatusCode.BadRequest, + Response = "BadRequest", + }; + var factory = new MockHttpClientFactory(handler); + + var client = new InstanceIdClient(factory, MockCredential); + + var exception = await Assert.ThrowsAsync( + () => client.SubscribeToTopicAsync("test-topic", new List { "abc123" })); + + Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode); + Assert.Equal("Unexpected HTTP response with status: 400 (BadRequest)\nBadRequest", exception.Message); + Assert.Null(exception.MessagingErrorCode); + Assert.NotNull(exception.HttpResponse); + Assert.Null(exception.InnerException); + } + + [Fact] + public async Task Unauthorized() + { + var handler = new MockMessageHandler() + { + StatusCode = HttpStatusCode.Unauthorized, + Response = "Unauthorized", + }; + var factory = new MockHttpClientFactory(handler); + + var client = new InstanceIdClient(factory, MockCredential); + + var exception = await Assert.ThrowsAsync( + () => client.SubscribeToTopicAsync("test-topic", new List { "abc123" })); + + Assert.Equal(ErrorCode.Unauthenticated, exception.ErrorCode); + Assert.Equal("Unexpected HTTP response with status: 401 (Unauthorized)\nUnauthorized", exception.Message); + Assert.Null(exception.MessagingErrorCode); + Assert.NotNull(exception.HttpResponse); + Assert.Null(exception.InnerException); + } + + [Fact] + public async Task Forbidden() + { + var handler = new MockMessageHandler() + { + StatusCode = HttpStatusCode.Forbidden, + Response = "Forbidden", + }; + var factory = new MockHttpClientFactory(handler); + + var client = new InstanceIdClient(factory, MockCredential); + + var exception = await Assert.ThrowsAsync( + () => client.SubscribeToTopicAsync("test-topic", new List { "abc123" })); + + Assert.Equal(ErrorCode.PermissionDenied, exception.ErrorCode); + Assert.Equal("Unexpected HTTP response with status: 403 (Forbidden)\nForbidden", exception.Message); + Assert.Null(exception.MessagingErrorCode); + Assert.NotNull(exception.HttpResponse); + Assert.Null(exception.InnerException); + } + + [Fact] + public async Task NotFound() + { + var handler = new MockMessageHandler() + { + StatusCode = HttpStatusCode.NotFound, + Response = "NotFound", + }; + var factory = new MockHttpClientFactory(handler); + + var client = new InstanceIdClient(factory, MockCredential); + + var exception = await Assert.ThrowsAsync( + () => client.SubscribeToTopicAsync("test-topic", new List { "abc123" })); + + Assert.Equal(ErrorCode.NotFound, exception.ErrorCode); + Assert.Equal("Unexpected HTTP response with status: 404 (NotFound)\nNotFound", exception.Message); + Assert.Null(exception.MessagingErrorCode); + Assert.NotNull(exception.HttpResponse); + Assert.Null(exception.InnerException); + } + + [Fact] + public async Task ServiceUnavailable() + { + var handler = new MockMessageHandler() + { + StatusCode = HttpStatusCode.ServiceUnavailable, + Response = "ServiceUnavailable", + }; + var factory = new MockHttpClientFactory(handler); + + var client = new InstanceIdClient(factory, MockCredential); + + var exception = await Assert.ThrowsAsync( + () => client.SubscribeToTopicAsync("test-topic", new List { "abc123" })); + + Assert.Equal(ErrorCode.Unavailable, exception.ErrorCode); + Assert.Equal("Unexpected HTTP response with status: 503 (ServiceUnavailable)\nServiceUnavailable", exception.Message); + Assert.Null(exception.MessagingErrorCode); + Assert.NotNull(exception.HttpResponse); + Assert.Null(exception.InnerException); + } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/TopicManagementResponseTest.cs b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/TopicManagementResponseTest.cs new file mode 100644 index 00000000..9853cfa6 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin.Tests/Messaging/TopicManagementResponseTest.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using FirebaseAdmin.Messaging; +using Newtonsoft.Json; +using Xunit; + +namespace FirebaseAdmin.Tests.Messaging +{ + public class TopicManagementResponseTest + { + [Fact] + public void SuccessfulReponse() + { + var json = @"{""results"": [{}, {}]}"; + var instanceIdServiceResponse = JsonConvert.DeserializeObject(json); + var response = new TopicManagementResponse(instanceIdServiceResponse); + + Assert.Equal(0, response.FailureCount); + Assert.Equal(2, response.SuccessCount); + } + + [Fact] + public void UnsuccessfulResponse() + { + var json = @"{""results"": [{}, {""error"":""NOT_FOUND""}]}"; + var instanceIdServiceResponse = JsonConvert.DeserializeObject(json); + var response = new TopicManagementResponse(instanceIdServiceResponse); + + Assert.Equal(1, response.FailureCount); + Assert.Equal(1, response.SuccessCount); + Assert.NotEmpty(response.Errors); + Assert.Equal("registration-token-not-registered", response.Errors[0].Reason); + Assert.Equal(1, response.Errors[0].Index); + } + + [Fact] + public void NullResponse() + { + Assert.Throws(() => + { + new TopicManagementResponse(null); + }); + } + + [Fact] + public void EmptyResponse() + { + Assert.Throws(() => + { + var instanceIdServiceResponse = new InstanceIdServiceResponse(); + new TopicManagementResponse(instanceIdServiceResponse); + }); + } + + [Fact] + public void UnregisteredToken() + { + var json = @"{""results"": [{}, {""error"":""NOT_FOUND""}]}"; + var instanceIdServiceResponse = JsonConvert.DeserializeObject(json); + var response = new TopicManagementResponse(instanceIdServiceResponse); + + Assert.Single(response.Errors); + Assert.Equal("registration-token-not-registered", response.Errors[0].Reason); + Assert.Equal(1, response.Errors[0].Index); + } + + [Fact] + public void CountsSuccessAndErrors() + { + var json = @"{""results"": [{""error"": ""NOT_FOUND""}, {}, {""error"": ""INVALID_ARGUMENT""}, {}, {}]}"; + var instanceIdServiceResponse = JsonConvert.DeserializeObject(json); + var response = new TopicManagementResponse(instanceIdServiceResponse); + + Assert.Equal(2, response.FailureCount); + Assert.Equal(3, response.SuccessCount); + Assert.Equal("registration-token-not-registered", response.Errors[0].Reason); + Assert.NotEmpty(response.Errors); + Assert.Equal(0, response.Errors[0].Index); + Assert.Equal("invalid-argument", response.Errors[1].Reason); + Assert.Equal(2, response.Errors[1].Index); + } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/ErrorInfo.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/ErrorInfo.cs new file mode 100644 index 00000000..79809609 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/ErrorInfo.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; + +namespace FirebaseAdmin.Messaging +{ + /// + /// A topic management error. + /// + public sealed class ErrorInfo + { + private static readonly string UnknownError = "unknown-error"; + + // Server error codes as defined in https://developers.google.com/instance-id/reference/server + // TODO: Should we handle other error codes here (e.g. PERMISSION_DENIED)? + private static readonly IReadOnlyDictionary ErrorCodes = new Dictionary + { + { "INVALID_ARGUMENT", "invalid-argument" }, + { "NOT_FOUND", "registration-token-not-registered" }, + { "INTERNAL", "internal-error" }, + { "TOO_MANY_TOPICS", "too-many-topics" }, + }; + + /// + /// Initializes a new instance of the class. + /// + /// Index of the error in the error codes. + /// Reason for the error. + public ErrorInfo(int index, string reason) + { + this.Index = index; + this.Reason = ErrorCodes.ContainsKey(reason) + ? ErrorCodes[reason] : UnknownError; + } + + /// + /// Gets the registration token to which this error is related to. + /// + /// An index into the original registration token list. + public int Index { get; private set; } + + /// + /// Gets the nature of the error. + /// + /// A non-null, non-empty error message. + public string Reason { get; private set; } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs index 4ad08b07..54bb0a2c 100644 --- a/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessaging.cs @@ -27,11 +27,15 @@ namespace FirebaseAdmin.Messaging public sealed class FirebaseMessaging : IFirebaseService { private readonly FirebaseMessagingClient messagingClient; + private readonly InstanceIdClient instanceIdClient; private FirebaseMessaging(FirebaseApp app) { this.messagingClient = new FirebaseMessagingClient( app.Options.HttpClientFactory, app.Options.Credential, app.GetProjectId()); + + this.instanceIdClient = new InstanceIdClient( + app.Options.HttpClientFactory, app.Options.Credential); } /// @@ -310,12 +314,35 @@ public async Task SendMulticastAsync( message.GetMessageList(), dryRun, cancellationToken).ConfigureAwait(false); } + /// + /// Subscribes a list of registration tokens to a topic. + /// + /// The topic name to subscribe to. /topics/ will be prepended to the topic name provided if absent. + /// A list of registration tokens to subscribe. + /// A task that completes with a , giving details about the topic subscription operations. + public async Task SubscribeToTopicAsync(string topic, List registrationTokens) + { + return await this.instanceIdClient.SubscribeToTopicAsync(topic, registrationTokens); + } + + /// + /// Unsubscribes a list of registration tokens from a topic. + /// + /// The topic name to unsubscribe from. /topics/ will be prepended to the topic name provided if absent. + /// A list of registration tokens to unsubscribe. + /// A task that completes with a , giving details about the topic unsubscription operations. + public async Task UnsubscribeFromTopicAsync(string topic, List registrationTokens) + { + return await this.instanceIdClient.UnsubscribeFromTopicAsync(topic, registrationTokens); + } + /// /// Deletes this service instance. /// void IFirebaseService.Delete() { this.messagingClient.Dispose(); + this.instanceIdClient.Dispose(); } } } diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/InstanceIdClient.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/InstanceIdClient.cs new file mode 100644 index 00000000..8aca8c0d --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/InstanceIdClient.cs @@ -0,0 +1,184 @@ +// Copyright 2019, 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Google.Apis.Auth.OAuth2; +using Google.Apis.Http; +using Google.Apis.Json; +using Google.Apis.Util; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// A helper class for interacting with the Firebase Instance ID service.Implements the FCM + /// topic management functionality. + /// + internal sealed class InstanceIdClient : IDisposable + { + private const string IidHost = "https://iid.googleapis.com"; + + private const string IidSubscriberPath = "iid/v1:batchAdd"; + + private const string IidUnsubscribePath = "iid/v1:batchRemove"; + + private readonly ConfigurableHttpClient httpClient; + + private readonly HttpErrorHandler errorHandler; + + /// + /// Initializes a new instance of the class. + /// + /// A default implentation of the HTTP client factory. + /// An instance of the GoogleCredential class. + public InstanceIdClient(HttpClientFactory clientFactory, GoogleCredential credential) + { + this.httpClient = clientFactory.ThrowIfNull(nameof(clientFactory)) + .CreateAuthorizedHttpClient(credential); + + this.errorHandler = new MessagingErrorHandler(); + } + + /// + /// Subscribes a list of registration tokens to a topic. + /// + /// The topic name to subscribe to. /topics/ will be prepended to the topic name provided if absent. + /// A list of registration tokens to subscribe. + /// A task that completes with a , giving details about the topic subscription operations. + public async Task SubscribeToTopicAsync(string topic, List registrationTokens) + { + return await this.SendInstanceIdRequest(topic, registrationTokens, IidSubscriberPath).ConfigureAwait(false); + } + + /// + /// Unsubscribes a list of registration tokens from a topic. + /// + /// The topic name to unsubscribe from. /topics/ will be prepended to the topic name provided if absent. + /// A list of registration tokens to unsubscribe. + /// A task that completes with a , giving details about the topic unsubscription operations. + public async Task UnsubscribeFromTopicAsync(string topic, List registrationTokens) + { + return await this.SendInstanceIdRequest(topic, registrationTokens, IidUnsubscribePath).ConfigureAwait(false); + } + + /// + /// Dispose the HttpClient. + /// + public void Dispose() + { + this.httpClient.Dispose(); + } + + private async Task SendInstanceIdRequest(string topic, List registrationTokens, string path) + { + this.ValidateRegistrationTokenList(registrationTokens); + + string url = $"{IidHost}/{path}"; + var body = new InstanceIdServiceRequest + { + Topic = this.GetPrefixedTopic(topic), + RegistrationTokens = registrationTokens, + }; + + var request = new HttpRequestMessage() + { + Method = HttpMethod.Post, + RequestUri = new Uri(url), + Content = NewtonsoftJsonSerializer.Instance.CreateJsonHttpContent(body), + }; + + request.Headers.Add("access_token_auth", "true"); + + try + { + var response = await this.httpClient.SendAsync(request, default(CancellationToken)).ConfigureAwait(false); + var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + this.errorHandler.ThrowIfError(response, json); + var instanceIdServiceResponse = JsonConvert.DeserializeObject(json); + return new TopicManagementResponse(instanceIdServiceResponse); + } + catch (HttpRequestException e) + { + throw this.CreateExceptionFromResponse(e); + } + catch (IOException) + { + throw new FirebaseMessagingException(ErrorCode.Internal, "Error while calling IID backend service"); + } + } + + private FirebaseMessagingException CreateExceptionFromResponse(HttpRequestException e) + { + var temp = e.ToFirebaseException(); + return new FirebaseMessagingException( + temp.ErrorCode, + temp.Message, + inner: temp.InnerException, + response: temp.HttpResponse); + } + + private void ValidateRegistrationTokenList(List registrationTokens) + { + if (registrationTokens == null) + { + throw new ArgumentNullException("Registration token list must not be null"); + } + + if (registrationTokens.Count() == 0) + { + throw new ArgumentException("Registration token list must not be empty"); + } + + if (registrationTokens.Count() > 1000) + { + throw new ArgumentException("Registration token list must not contain more than 1000 tokens"); + } + + foreach (var registrationToken in registrationTokens) + { + if (string.IsNullOrEmpty(registrationToken)) + { + throw new ArgumentException("Registration tokens must not be null or empty"); + } + } + } + + private string GetPrefixedTopic(string topic) + { + if (topic.StartsWith("/topics/")) + { + return topic; + } + else + { + return "/topics/" + topic; + } + } + + private class InstanceIdServiceRequest + { + [JsonProperty("to")] + public string Topic { get; set; } + + [JsonProperty("registration_tokens")] + public List RegistrationTokens { get; set; } + } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/InstanceIdServiceResponse.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/InstanceIdServiceResponse.cs new file mode 100644 index 00000000..16d7eba1 --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/InstanceIdServiceResponse.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace FirebaseAdmin.Messaging +{ + /// + /// Response from an operation that subscribes or unsubscribes registration tokens to a topic. + /// See and . + /// + internal class InstanceIdServiceResponse + { + /// + /// Gets the errors returned by the operation. + /// + [JsonProperty("results")] + public List Results { get; private set; } + + /// + /// Gets the number of errors returned by the operation. + /// + public int ErrorCount => Results?.Count(results => results.HasError) ?? 0; + + /// + /// Gets the number of results returned by the operation. + /// + public int ResultCount => Results?.Count() ?? 0; + + /// + /// An instance Id response error. + /// + internal class InstanceIdServiceResponseElement + { + /// + /// Gets a value indicating the error in this element of the response array. If this is empty this indicates success. + /// + [JsonProperty("error")] + public string Error { get; private set; } + + /// + /// Gets a value indicating whether this response element in the response array is an error, as an empty element indicates success. + /// + public bool HasError => !string.IsNullOrEmpty(Error); + } + } +} diff --git a/FirebaseAdmin/FirebaseAdmin/Messaging/TopicManagementResponse.cs b/FirebaseAdmin/FirebaseAdmin/Messaging/TopicManagementResponse.cs new file mode 100644 index 00000000..f9e86cdc --- /dev/null +++ b/FirebaseAdmin/FirebaseAdmin/Messaging/TopicManagementResponse.cs @@ -0,0 +1,76 @@ +// Copyright 2019, 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; + +namespace FirebaseAdmin.Messaging +{ + /// + /// The response produced by FCM topic management operations. + /// + public sealed class TopicManagementResponse + { + /// + /// Initializes a new instance of the class. + /// + /// The results from the response produced by FCM topic management operations. + internal TopicManagementResponse(InstanceIdServiceResponse instanceIdServiceResponse) + { + if (instanceIdServiceResponse == null) + { + throw new ArgumentNullException("Unexpected null response from topic management service"); + } + + if (instanceIdServiceResponse.ResultCount == 0) + { + throw new ArgumentNullException("Unexpected empty response from topic management service"); + } + + var resultErrors = new List(); + for (var i = 0; i < instanceIdServiceResponse.Results.Count; i++) + { + var result = instanceIdServiceResponse.Results[i]; + if (result.HasError) + { + resultErrors.Add(new ErrorInfo(i, result.Error)); + } + else + { + this.SuccessCount++; + } + } + + this.Errors = resultErrors; + } + + /// + /// Gets the number of registration tokens that were successfully subscribed or unsubscribed. + /// + /// The number of registration tokens that were successfully subscribed or unsubscribed. + public int SuccessCount { get; private set; } + + /// + /// Gets the number of registration tokens that could not be subscribed or unsubscribed, and resulted in an error. + /// + /// The number of failures. + public int FailureCount => this.Errors.Count; + + /// + /// Gets a list of errors encountered while executing the topic management operation. + /// + /// A non-null list. + public IReadOnlyList Errors { get; private set; } + } +}