Skip to content

Commit 0bd3a8d

Browse files
committed
Exposing FirebaseMessagingException from SendResponse; Additional tests
1 parent 226269b commit 0bd3a8d

File tree

5 files changed

+238
-31
lines changed

5 files changed

+238
-31
lines changed

FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs

Lines changed: 163 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -207,12 +207,11 @@ public async Task SendAllAsyncWithError()
207207
""error"": {
208208
""code"": 400,
209209
""message"": ""The registration token is not a valid FCM registration token"",
210-
""errors"": [
211-
{
212-
""message"": ""The registration token is not a valid FCM registration token"",
213-
""domain"": ""global"",
214-
""reason"": ""badRequest""
215-
}
210+
""details"": [
211+
{
212+
""@type"": ""type.googleapis.com/google.firebase.fcm.v1.FcmError"",
213+
""errorCode"": ""UNREGISTERED""
214+
}
216215
],
217216
""status"": ""INVALID_ARGUMENT""
218217
}
@@ -245,7 +244,87 @@ public async Task SendAllAsyncWithError()
245244
Assert.Equal(1, response.SuccessCount);
246245
Assert.Equal(1, response.FailureCount);
247246
Assert.Equal("projects/fir-adminintegrationtests/messages/8580920590356323124", response.Responses[0].MessageId);
248-
Assert.NotNull(response.Responses[1].Exception);
247+
248+
var exception = response.Responses[1].Exception;
249+
Assert.NotNull(exception);
250+
Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode);
251+
Assert.Equal("The registration token is not a valid FCM registration token", exception.Message);
252+
Assert.Equal(MessagingErrorCode.Unregistered, exception.MessagingErrorCode);
253+
Assert.NotNull(exception.HttpResponse);
254+
255+
Assert.Equal(1, handler.Calls);
256+
var versionHeader = $"X-Firebase-Client: {FirebaseMessagingClient.ClientVersion}";
257+
Assert.Equal(2, this.CountLinesWithPrefix(handler.LastRequestBody, versionHeader));
258+
}
259+
260+
[Fact]
261+
public async Task SendAllAsyncWithErrorNoDetail()
262+
{
263+
var rawResponse = @"
264+
--batch_test-boundary
265+
Content-Type: application/http
266+
Content-ID: response-
267+
268+
HTTP/1.1 200 OK
269+
Content-Type: application/json; charset=UTF-8
270+
Vary: Origin
271+
Vary: X-Origin
272+
Vary: Referer
273+
274+
{
275+
""name"": ""projects/fir-adminintegrationtests/messages/8580920590356323124""
276+
}
277+
278+
--batch_test-boundary
279+
Content-Type: application/http
280+
Content-ID: response-
281+
282+
HTTP/1.1 400 Bad Request
283+
Content-Type: application/json; charset=UTF-8
284+
285+
{
286+
""error"": {
287+
""code"": 400,
288+
""message"": ""The registration token is not a valid FCM registration token"",
289+
""status"": ""INVALID_ARGUMENT""
290+
}
291+
}
292+
293+
--batch_test-boundary
294+
";
295+
var handler = new MockMessageHandler()
296+
{
297+
Response = rawResponse,
298+
ApplyHeaders = (_, headers) =>
299+
{
300+
headers.Remove("Content-Type");
301+
headers.TryAddWithoutValidation("Content-Type", "multipart/mixed; boundary=batch_test-boundary");
302+
},
303+
};
304+
var factory = new MockHttpClientFactory(handler);
305+
var client = new FirebaseMessagingClient(factory, MockCredential, "test-project");
306+
var message1 = new Message()
307+
{
308+
Token = "test-token1",
309+
};
310+
var message2 = new Message()
311+
{
312+
Token = "test-token2",
313+
};
314+
315+
var response = await client.SendAllAsync(new[] { message1, message2 });
316+
317+
Assert.Equal(1, response.SuccessCount);
318+
Assert.Equal(1, response.FailureCount);
319+
Assert.Equal("projects/fir-adminintegrationtests/messages/8580920590356323124", response.Responses[0].MessageId);
320+
321+
var exception = response.Responses[1].Exception;
322+
Assert.NotNull(exception);
323+
Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode);
324+
Assert.Equal("The registration token is not a valid FCM registration token", exception.Message);
325+
Assert.Null(exception.MessagingErrorCode);
326+
Assert.NotNull(exception.HttpResponse);
327+
249328
Assert.Equal(1, handler.Calls);
250329
var versionHeader = $"X-Firebase-Client: {FirebaseMessagingClient.ClientVersion}";
251330
Assert.Equal(2, this.CountLinesWithPrefix(handler.LastRequestBody, versionHeader));
@@ -279,6 +358,82 @@ public async Task SendAllAsyncWithTooManyMessages()
279358

280359
[Fact]
281360
public async Task HttpErrorAsync()
361+
{
362+
var handler = new MockMessageHandler()
363+
{
364+
StatusCode = HttpStatusCode.InternalServerError,
365+
Response = @"{
366+
""error"": {
367+
""status"": ""PERMISSION_DENIED"",
368+
""message"": ""test error"",
369+
""details"": [
370+
{
371+
""@type"": ""type.googleapis.com/google.firebase.fcm.v1.FcmError"",
372+
""errorCode"": ""UNREGISTERED""
373+
}
374+
]
375+
}
376+
}",
377+
};
378+
var factory = new MockHttpClientFactory(handler);
379+
var client = new FirebaseMessagingClient(factory, MockCredential, "test-project");
380+
var message = new Message()
381+
{
382+
Topic = "test-topic",
383+
};
384+
385+
var ex = await Assert.ThrowsAsync<FirebaseMessagingException>(
386+
async () => await client.SendAsync(message));
387+
388+
Assert.Equal(ErrorCode.PermissionDenied, ex.ErrorCode);
389+
Assert.Equal("test error", ex.Message);
390+
Assert.Equal(MessagingErrorCode.Unregistered, ex.MessagingErrorCode);
391+
Assert.NotNull(ex.HttpResponse);
392+
393+
var req = JsonConvert.DeserializeObject<FirebaseMessagingClient.SendRequest>(
394+
handler.LastRequestBody);
395+
Assert.Equal("test-topic", req.Message.Topic);
396+
Assert.False(req.ValidateOnly);
397+
Assert.Equal(1, handler.Calls);
398+
}
399+
400+
[Fact]
401+
public async Task HttpErrorNoDetailAsync()
402+
{
403+
var handler = new MockMessageHandler()
404+
{
405+
StatusCode = HttpStatusCode.InternalServerError,
406+
Response = @"{
407+
""error"": {
408+
""status"": ""PERMISSION_DENIED"",
409+
""message"": ""test error""
410+
}
411+
}",
412+
};
413+
var factory = new MockHttpClientFactory(handler);
414+
var client = new FirebaseMessagingClient(factory, MockCredential, "test-project");
415+
var message = new Message()
416+
{
417+
Topic = "test-topic",
418+
};
419+
420+
var ex = await Assert.ThrowsAsync<FirebaseMessagingException>(
421+
async () => await client.SendAsync(message));
422+
423+
Assert.Equal(ErrorCode.PermissionDenied, ex.ErrorCode);
424+
Assert.Equal("test error", ex.Message);
425+
Assert.Null(ex.MessagingErrorCode);
426+
Assert.NotNull(ex.HttpResponse);
427+
428+
var req = JsonConvert.DeserializeObject<FirebaseMessagingClient.SendRequest>(
429+
handler.LastRequestBody);
430+
Assert.Equal("test-topic", req.Message.Topic);
431+
Assert.False(req.ValidateOnly);
432+
Assert.Equal(1, handler.Calls);
433+
}
434+
435+
[Fact]
436+
public async Task HttpErrorNonJsonAsync()
282437
{
283438
var handler = new MockMessageHandler()
284439
{
@@ -297,7 +452,7 @@ public async Task HttpErrorAsync()
297452

298453
Assert.Equal(ErrorCode.Internal, ex.ErrorCode);
299454
Assert.Equal(
300-
$"Unexpected HTTP response with status: {500} (InternalServerError)\nnot json",
455+
"Unexpected HTTP response with status: 500 (InternalServerError)\nnot json",
301456
ex.Message);
302457
Assert.Null(ex.MessagingErrorCode);
303458
Assert.NotNull(ex.HttpResponse);

FirebaseAdmin/FirebaseAdmin.Tests/Messaging/MessagingErrorHandlerTest.cs

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
using System.Collections.Generic;
1516
using System.Net;
1617
using System.Net.Http;
1718
using System.Text;
@@ -21,6 +22,19 @@ namespace FirebaseAdmin.Messaging.Tests
2122
{
2223
public class MessagingErrorHandlerTest
2324
{
25+
public static readonly IEnumerable<object[]> MessagingErrorCodes =
26+
new List<object[]>()
27+
{
28+
new object[] { "APNS_AUTH_ERROR", MessagingErrorCode.ThirdPartyAuthError },
29+
new object[] { "INTERNAL", MessagingErrorCode.Internal },
30+
new object[] { "INVALID_ARGUMENT", MessagingErrorCode.InvalidArgument },
31+
new object[] { "QUOTA_EXCEEDED", MessagingErrorCode.QuotaExceeded },
32+
new object[] { "SENDER_ID_MISMATCH", MessagingErrorCode.SenderIdMismatch },
33+
new object[] { "THIRD_PARTY_AUTH_ERROR", MessagingErrorCode.ThirdPartyAuthError },
34+
new object[] { "UNAVAILABLE", MessagingErrorCode.Unavailable },
35+
new object[] { "UNREGISTERED", MessagingErrorCode.Unregistered },
36+
};
37+
2438
[Fact]
2539
public void PlatformError()
2640
{
@@ -46,8 +60,40 @@ public void PlatformError()
4660
Assert.Null(error.InnerException);
4761
}
4862

63+
[Theory]
64+
[MemberData(nameof(MessagingErrorCodes))]
65+
public void KnownMessagingErrorCode(string code, MessagingErrorCode expected)
66+
{
67+
var json = $@"{{
68+
""error"": {{
69+
""status"": ""PERMISSION_DENIED"",
70+
""message"": ""Test error message"",
71+
""details"": [
72+
{{
73+
""@type"": ""type.googleapis.com/google.firebase.fcm.v1.FcmError"",
74+
""errorCode"": ""{code}""
75+
}}
76+
]
77+
}}
78+
}}";
79+
var resp = new HttpResponseMessage()
80+
{
81+
StatusCode = HttpStatusCode.ServiceUnavailable,
82+
Content = new StringContent(json, Encoding.UTF8, "application/json"),
83+
};
84+
85+
var handler = new MessagingErrorHandler();
86+
var error = Assert.Throws<FirebaseMessagingException>(() => handler.ThrowIfError(resp, json));
87+
88+
Assert.Equal(ErrorCode.PermissionDenied, error.ErrorCode);
89+
Assert.Equal("Test error message", error.Message);
90+
Assert.Equal(expected, error.MessagingErrorCode);
91+
Assert.Same(resp, error.HttpResponse);
92+
Assert.Null(error.InnerException);
93+
}
94+
4995
[Fact]
50-
public void PlatformErrorWithMessagingErrorCode()
96+
public void UnknownMessagingErrorCode()
5197
{
5298
var json = @"{
5399
""error"": {
@@ -56,7 +102,7 @@ public void PlatformErrorWithMessagingErrorCode()
56102
""details"": [
57103
{
58104
""@type"": ""type.googleapis.com/google.firebase.fcm.v1.FcmError"",
59-
""errorCode"": ""UNREGISTERED""
105+
""errorCode"": ""SOMETHING_UNUSUAL""
60106
}
61107
]
62108
}
@@ -72,13 +118,13 @@ public void PlatformErrorWithMessagingErrorCode()
72118

73119
Assert.Equal(ErrorCode.PermissionDenied, error.ErrorCode);
74120
Assert.Equal("Test error message", error.Message);
75-
Assert.Equal(MessagingErrorCode.Unregistered, error.MessagingErrorCode);
121+
Assert.Null(error.MessagingErrorCode);
76122
Assert.Same(resp, error.HttpResponse);
77123
Assert.Null(error.InnerException);
78124
}
79125

80126
[Fact]
81-
public void PlatformErrorWithoutAnyDetails()
127+
public void NoDetails()
82128
{
83129
var json = @"{}";
84130
var resp = new HttpResponseMessage()

FirebaseAdmin/FirebaseAdmin/Messaging/FirebaseMessagingClient.cs

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -166,20 +166,6 @@ private static void AddCommonHeaders(HttpRequestMessage request)
166166
request.Headers.Add("X-Firebase-Client", ClientVersion);
167167
}
168168

169-
private async Task<FirebaseMessagingException> CreateExceptionFor(HttpResponseMessage response)
170-
{
171-
var json = await response.Content.ReadAsStringAsync();
172-
try
173-
{
174-
this.errorHandler.ThrowIfError(response, json);
175-
return null;
176-
}
177-
catch (FirebaseMessagingException ex)
178-
{
179-
return ex;
180-
}
181-
}
182-
183169
private async Task<HttpResponseMessage> SendRequestAsync(object body, CancellationToken cancellationToken)
184170
{
185171
var request = new HttpRequestMessage()
@@ -204,21 +190,24 @@ private async Task<BatchResponse> SendBatchRequestAsync(
204190
dryRun,
205191
async (content, error, index, message) =>
206192
{
193+
SendResponse sendResponse;
207194
if (error != null)
208195
{
209-
responses.Add(SendResponse.FromException(await this.CreateExceptionFor(message)));
196+
sendResponse = SendResponse.FromException(await this.CreateExceptionFor(message));
210197
}
211198
else if (content != null)
212199
{
213-
responses.Add(SendResponse.FromMessageId(content.Name));
200+
sendResponse = SendResponse.FromMessageId(content.Name);
214201
}
215202
else
216203
{
217204
var exception = new FirebaseMessagingException(
218205
ErrorCode.Unknown,
219206
$"Unexpected batch response. Response status code: {message.StatusCode}.");
220-
responses.Add(SendResponse.FromException(exception));
207+
sendResponse = SendResponse.FromException(exception);
221208
}
209+
210+
responses.Add(sendResponse);
222211
});
223212

224213
await batch.ExecuteAsync(cancellationToken).ConfigureAwait(false);
@@ -245,6 +234,20 @@ private BatchRequest CreateBatchRequest(
245234
return batch;
246235
}
247236

237+
private async Task<FirebaseMessagingException> CreateExceptionFor(HttpResponseMessage response)
238+
{
239+
var json = await response.Content.ReadAsStringAsync();
240+
try
241+
{
242+
this.errorHandler.ThrowIfError(response, json);
243+
throw new InvalidOperationException("Invalid batch response");
244+
}
245+
catch (FirebaseMessagingException ex)
246+
{
247+
return ex;
248+
}
249+
}
250+
248251
/// <summary>
249252
/// Represents the envelope message accepted by the FCM backend service, including the message
250253
/// payload and other options like <c>validate_only</c>.

FirebaseAdmin/FirebaseAdmin/Messaging/SendResponse.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ private SendResponse(string messageId)
2727
this.MessageId = messageId;
2828
}
2929

30-
private SendResponse(FirebaseException exception)
30+
private SendResponse(FirebaseMessagingException exception)
3131
{
3232
this.Exception = exception;
3333
}
@@ -40,7 +40,7 @@ private SendResponse(FirebaseException exception)
4040
/// <summary>
4141
/// Gets an exception if the send operation failed. Otherwise returns null.
4242
/// </summary>
43-
public FirebaseException Exception { get; }
43+
public FirebaseMessagingException Exception { get; }
4444

4545
/// <summary>
4646
/// Gets a value indicating whether the send operation was successful or not. When this property

FirebaseAdmin/FirebaseAdmin/PlatformErrorHandler.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,9 @@ private PlatformErrorResponse ParseResponseBody(string body)
6464
}
6565
catch
6666
{
67+
// Ignore any error that may occur while parsing the error response. The server
68+
// may have responded with a non-json payload. Return an empty return value, and
69+
// let the base class logic come into play.
6770
return new PlatformErrorResponse();
6871
}
6972
}

0 commit comments

Comments
 (0)