Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,14 @@ public async Task BadRequest()

var client = new InstanceIdClient(factory, MockCredential);

await Assert.ThrowsAsync<FirebaseMessagingException>(
var exception = await Assert.ThrowsAsync<FirebaseMessagingException>(
() => client.SubscribeToTopicAsync("test-topic", new List<string> { "abc123" }));

Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode);
Assert.Equal("Unexpected HTTP response with status: 400 (Bad Request)\nBad Request", exception.Message);
Assert.Null(exception.MessagingErrorCode);
Assert.NotNull(exception.HttpResponse);
Assert.Null(exception.InnerException);
}

[Fact]
Expand All @@ -90,8 +96,14 @@ public async Task Unauthorized()

var client = new InstanceIdClient(factory, MockCredential);

await Assert.ThrowsAsync<FirebaseMessagingException>(
var exception = await Assert.ThrowsAsync<FirebaseMessagingException>(
() => client.SubscribeToTopicAsync("test-topic", new List<string> { "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]
Expand All @@ -106,8 +118,14 @@ public async Task Forbidden()

var client = new InstanceIdClient(factory, MockCredential);

await Assert.ThrowsAsync<FirebaseMessagingException>(
var exception = await Assert.ThrowsAsync<FirebaseMessagingException>(
() => client.SubscribeToTopicAsync("test-topic", new List<string> { "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]
Expand All @@ -122,8 +140,14 @@ public async Task NotFound()

var client = new InstanceIdClient(factory, MockCredential);

await Assert.ThrowsAsync<FirebaseMessagingException>(
var exception = await Assert.ThrowsAsync<FirebaseMessagingException>(
() => client.SubscribeToTopicAsync("test-topic", new List<string> { "abc123" }));

Assert.Equal(ErrorCode.NotFound, exception.ErrorCode);
Assert.Equal("Unexpected HTTP response with status: 404 (Not Found)\nNot Found", exception.Message);
Assert.Null(exception.MessagingErrorCode);
Assert.NotNull(exception.HttpResponse);
Assert.Null(exception.InnerException);
}

[Fact]
Expand All @@ -138,8 +162,14 @@ public async Task ServiceUnavailable()

var client = new InstanceIdClient(factory, MockCredential);

await Assert.ThrowsAsync<FirebaseMessagingException>(
var exception = await Assert.ThrowsAsync<FirebaseMessagingException>(
() => client.SubscribeToTopicAsync("test-topic", new List<string> { "abc123" }));

Assert.Equal(ErrorCode.Unavailable, exception.ErrorCode);
Assert.Equal("Unexpected HTTP response with status: 503 (Service Unavailable)\nService Unavailable", exception.Message);
Assert.Null(exception.MessagingErrorCode);
Assert.NotNull(exception.HttpResponse);
Assert.Null(exception.InnerException);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,22 @@ public class TopicManagementResponseTest
[Fact]
public void SuccessfulReponse()
{
var topicManagementResults = new List<string> { null };
var response = new TopicManagementResponse(topicManagementResults);
var json = @"{""results"": [{}, {}]}";
var instanceIdServiceResponse = JsonConvert.DeserializeObject<InstanceIdServiceResponse>(json);
var response = new TopicManagementResponse(instanceIdServiceResponse);

Assert.Empty(response.Errors);
Assert.Equal(1, response.SuccessCount);
Assert.Equal(0, response.FailureCount);
Assert.Equal(2, response.SuccessCount);
}

[Fact]
public void UnsuccessfulResponse()
{
var topicManagementResults = new List<string> { null, "NOT_FOUND" };
var response = new TopicManagementResponse(topicManagementResults);
var json = @"{""results"": [{}, {""error"":""NOT_FOUND""}]}";
var instanceIdServiceResponse = JsonConvert.DeserializeObject<InstanceIdServiceResponse>(json);
var response = new TopicManagementResponse(instanceIdServiceResponse);

Assert.Single(response.Errors);
Assert.Equal(1, response.FailureCount);
Assert.Equal(1, response.SuccessCount);
Assert.NotEmpty(response.Errors);
Assert.Equal("registration-token-not-registered", response.Errors[0].Reason);
Expand All @@ -45,15 +47,17 @@ public void EmptyResponse()
{
Assert.Throws<ArgumentException>(() =>
{
new TopicManagementResponse(new List<string>());
var instanceIdServiceResponse = new InstanceIdServiceResponse();
new TopicManagementResponse(instanceIdServiceResponse);
});
}

[Fact]
public void UnknownError()
{
var topicManagementResults = new List<string> { "NOT_A_REAL_ERROR_CODE" };
var response = new TopicManagementResponse(topicManagementResults);
var json = @"{""results"": [{}, {""error"":""NOT_FOUND""}]}";
var instanceIdServiceResponse = JsonConvert.DeserializeObject<InstanceIdServiceResponse>(json);
var response = new TopicManagementResponse(instanceIdServiceResponse);

Assert.Single(response.Errors);
Assert.Equal("unknown-error", response.Errors[0].Reason);
Expand All @@ -63,8 +67,9 @@ public void UnknownError()
[Fact]
public void UnexpectedResponse()
{
var topicManagementResults = new List<string> { "NOT_A_REAL_CODE" };
var response = new TopicManagementResponse(topicManagementResults);
var json = @"{""results"": [{""unexpected"":""NOT_A_REAL_CODE""}]}";
var instanceIdServiceResponse = JsonConvert.DeserializeObject<InstanceIdServiceResponse>(json);
var response = new TopicManagementResponse(instanceIdServiceResponse);

Assert.Single(response.Errors);
Assert.Equal("unknown-error", response.Errors[0].Reason);
Expand All @@ -74,8 +79,9 @@ public void UnexpectedResponse()
[Fact]
public void CountsSuccessAndErrors()
{
var topicManagementResults = new List<string> { "NOT_FOUND", null, "INVALID_ARGUMENT", null, null };
var response = new TopicManagementResponse(topicManagementResults);
var json = @"{""results"": [{""error"": ""NOT_FOUND""}, {}, {""error"": ""INVALID_ARGUMENT""}, {}, {}]}";
var instanceIdServiceResponse = JsonConvert.DeserializeObject<InstanceIdServiceResponse>(json);
var response = new TopicManagementResponse(instanceIdServiceResponse);

Assert.Equal(2, response.FailureCount);
Assert.Equal(3, response.SuccessCount);
Expand Down
23 changes: 11 additions & 12 deletions FirebaseAdmin/FirebaseAdmin/Messaging/ErrorInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ namespace FirebaseAdmin.Messaging
/// </summary>
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 IReadOnlyDictionary<string, string> errorCodes;
private readonly string unknownError = "unknown-error";
private static readonly IReadOnlyDictionary<string, string> ErrorCodes = new Dictionary<string, string>
{
{ "INVALID_ARGUMENT", "invalid-argument" },
{ "NOT_FOUND", "registration-token-not-registered" },
{ "INTERNAL", "internal-error" },
{ "TOO_MANY_TOPICS", "too-many-topics" },
};

/// <summary>
/// Initializes a new instance of the <see cref="ErrorInfo"/> class.
Expand All @@ -19,17 +26,9 @@ public sealed class ErrorInfo
/// <param name="reason">Reason for the error.</param>
public ErrorInfo(int index, string reason)
{
errorCodes = new Dictionary<string, string>
{
{ "INVALID_ARGUMENT", "invalid-argument" },
{ "NOT_FOUND", "registration-token-not-registered" },
{ "INTERNAL", "internal-error" },
{ "TOO_MANY_TOPICS", "too-many-topics" },
};

this.Index = index;
this.Reason = errorCodes.ContainsKey(reason)
? errorCodes[reason] : this.unknownError;
this.Reason = ErrorCodes.ContainsKey(reason)
? ErrorCodes[reason] : UnknownError;
}

/// <summary>
Expand Down
29 changes: 5 additions & 24 deletions FirebaseAdmin/FirebaseAdmin/Messaging/InstanceIdClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,7 @@ private async Task<TopicManagementResponse> SendInstanceIdRequest(string topic,
throw new ArgumentException("unexpected response from topic management service");
}

var results = instanceIdServiceResponse.Results.Select(r => r.Error).ToList();
return new TopicManagementResponse(results);
return new TopicManagementResponse(instanceIdServiceResponse);
}
catch (HttpRequestException e)
{
Expand All @@ -145,24 +144,24 @@ private void ValidateRegistrationTokenList(List<string> registrationTokens)
{
if (registrationTokens == null)
{
throw new FirebaseMessagingException(ErrorCode.InvalidArgument, "Registration token list must not be null");
throw new ArgumentNullException("Registration token list must not be null");
}

if (registrationTokens.Count() == 0)
{
throw new FirebaseMessagingException(ErrorCode.InvalidArgument, "Registration token list must not be empty");
throw new ArgumentException("Registration token list must not be empty");
}

if (registrationTokens.Count() > 1000)
{
throw new FirebaseMessagingException(ErrorCode.InvalidArgument, "Registration token list must not contain more than 1000 tokens");
throw new ArgumentException("Registration token list must not contain more than 1000 tokens");
}

foreach (var registrationToken in registrationTokens)
{
if (string.IsNullOrEmpty(registrationToken))
{
throw new FirebaseMessagingException(ErrorCode.InvalidArgument, "Registration token must not be null");
throw new ArgumentNullException("Registration tokens must not be null");
}
}
}
Expand All @@ -187,23 +186,5 @@ private class InstanceIdServiceRequest
[JsonProperty("registration_tokens")]
public List<string> RegistrationTokens { get; set; }
}

private class InstanceIdServiceResponse
{
[JsonProperty("results")]
public List<InstanceIdServiceResponseElement> Results { get; private set; }

public int ErrorCount => Results?.Count(results => results.HasError) ?? 0;

public int ResultCount => Results?.Count() ?? 0;

public class InstanceIdServiceResponseElement
{
[JsonProperty("error")]
public string Error { get; private set; }

public bool HasError => !string.IsNullOrEmpty(Error);
}
}
}
}
46 changes: 46 additions & 0 deletions FirebaseAdmin/FirebaseAdmin/Messaging/InstanceIdServiceResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;

namespace FirebaseAdmin.Messaging
{
/// <summary>
/// Response from an operation that subscribes or unsubscribes registration tokens to a topic.
/// See <see cref="FirebaseMessaging.SubscribeToTopicAsync(string, List{string})"/> and <see cref="FirebaseMessaging.UnsubscribeFromTopicAsync(string, List{string})"/>.
/// </summary>
internal class InstanceIdServiceResponse
{
/// <summary>
/// Gets the errors returned by the operation.
/// </summary>
[JsonProperty("results")]
public List<InstanceIdServiceResponseElement> Results { get; private set; }

/// <summary>
/// Gets the number of errors returned by the operation.
/// </summary>
public int ErrorCount => Results?.Count(results => results.HasError) ?? 0;

/// <summary>
/// Gets the number of results returned by the operation.
/// </summary>
public int ResultCount => Results?.Count() ?? 0;

/// <summary>
/// An instance Id response error.
/// </summary>
public class InstanceIdServiceResponseElement
{
/// <summary>
/// Gets a value indicating the error in this element of the response array. If this is empty this indicates success.
/// </summary>
[JsonProperty("error")]
public string Error { get; private set; }

/// <summary>
/// Gets a value indicating whether this response element in the response array is an error, as an empty element indicates success.
/// </summary>
public bool HasError => !string.IsNullOrEmpty(Error);
}
}
}
22 changes: 8 additions & 14 deletions FirebaseAdmin/FirebaseAdmin/Messaging/TopicManagementResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

using System;
using System.Collections.Generic;
using System.Linq;

namespace FirebaseAdmin.Messaging
{
Expand All @@ -26,26 +25,21 @@ public sealed class TopicManagementResponse
/// <summary>
/// Initializes a new instance of the <see cref="TopicManagementResponse"/> class.
/// </summary>
/// <param name="topicManagementResults">The results from the response produced by FCM topic management operations.</param>
public TopicManagementResponse(List<string> topicManagementResults)
/// <param name="instanceIdServiceResponse">The results from the response produced by FCM topic management operations.</param>
internal TopicManagementResponse(InstanceIdServiceResponse instanceIdServiceResponse)
{
if (topicManagementResults == null)
if (instanceIdServiceResponse == null || instanceIdServiceResponse.ResultCount == 0)
{
throw new ArgumentNullException("Topic management response list is null");
}

if (topicManagementResults.Count() == 0)
{
throw new ArgumentException("Topic management response list is empty");
throw new ArgumentException("unexpected response from topic management service");
}

var resultErrors = new List<ErrorInfo>();
for (var i = 0; i < topicManagementResults.Count(); i++)
for (var i = 0; i < instanceIdServiceResponse.Results.Count; i++)
{
var topicManagementResult = topicManagementResults[i];
if (!string.IsNullOrEmpty(topicManagementResult))
var result = instanceIdServiceResponse.Results[i];
if (result.HasError)
{
resultErrors.Add(new ErrorInfo(i, topicManagementResult));
resultErrors.Add(new ErrorInfo(i, result.Error));
}
else
{
Expand Down