Skip to content

Commit

Permalink
feat(fcm): Add SendEachAsync and SendEachForMulticastAsync for FC…
Browse files Browse the repository at this point in the history
…M batch send (#348)

* feat(fcm): Implement `SendEachAsync` and `SendEachForMulticastAsync` (#343)

* Implement `SendEachAsync` and `SendEachForMulticastAsync`

`SendEachAsync` vs `SendAllAsync`
1. `SendEachAsync` sends one HTTP request to V1 Send endpoint for each
    message in the list.
   `SendAllAsync` sends only one HTTP request to V1 Batch Send endpoint
    to send all messages in the list.
2. `SendEachAsync` calls `Task.WhenAll` to wait for all
    `httpClient.SendAndDeserializeAsync` calls to complete and construct
    a `BatchResponse` with all `SendResponse`s.
    An `httpClient.SendAndDeserializeAsync` call to V1 Send endpoint
    either completes with a success or throws an exception. So if an
    exception is thrown out, the exception will be caught in `SendEachAsync`
    and turned into a `SendResponse` with an exception.
    Therefore, unlike `SendAllAsync`, `SendEachAsync` does not always throw
    an exception for a total failure. It can also return a `BatchResponse`
    with only exceptions in it.

`SendEachForMulticastAsync` calls `SendEachAsync` under the hood.

* Add integration tests for the batch-send reimplementation:
SendEach() and SendEachForMulticast()

* Address the doc review comments
  • Loading branch information
Doris-Ge authored Jun 9, 2023
1 parent 0870aa6 commit 74bd9e5
Show file tree
Hide file tree
Showing 5 changed files with 544 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,78 @@ public async Task Send()
Assert.Matches(new Regex("^projects/.*/messages/.*$"), id);
}

[Fact]
public async Task SendEach()
{
var message1 = new Message()
{
Topic = "foo-bar",
Notification = new Notification()
{
Title = "Title",
Body = "Body",
ImageUrl = "https://example.com/image.png",
},
Android = new AndroidConfig()
{
Priority = Priority.Normal,
TimeToLive = TimeSpan.FromHours(1),
RestrictedPackageName = "com.google.firebase.testing",
},
};
var message2 = new Message()
{
Topic = "fiz-buz",
Notification = new Notification()
{
Title = "Title",
Body = "Body",
},
Android = new AndroidConfig()
{
Priority = Priority.Normal,
TimeToLive = TimeSpan.FromHours(1),
RestrictedPackageName = "com.google.firebase.testing",
},
};
var response = await FirebaseMessaging.DefaultInstance.SendEachAsync(new[] { message1, message2 }, dryRun: true);
Assert.NotNull(response);
Assert.Equal(2, response.SuccessCount);
Assert.True(!string.IsNullOrEmpty(response.Responses[0].MessageId));
Assert.Matches(new Regex("^projects/.*/messages/.*$"), response.Responses[0].MessageId);
Assert.True(!string.IsNullOrEmpty(response.Responses[1].MessageId));
Assert.Matches(new Regex("^projects/.*/messages/.*$"), response.Responses[1].MessageId);
}

[Fact]
public async Task SendEachForMulticast()
{
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",
},
Tokens = new[]
{
"token1",
"token2",
},
};
var response = await FirebaseMessaging.DefaultInstance.SendEachForMulticastAsync(multicastMessage, dryRun: true);
Assert.NotNull(response);
Assert.Equal(2, response.FailureCount);
Assert.NotNull(response.Responses[0].Exception);
Assert.NotNull(response.Responses[1].Exception);
}

[Fact]
public async Task SendAll()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,212 @@ public async Task SendDryRunAsync()
this.CheckHeaders(handler.LastRequestHeaders);
}

[Fact]
public async Task SendEachAsync()
{
var handler = new MockMessageHandler()
{
GenerateResponse = (incomingRequest) =>
{
string name;
if (incomingRequest.Body.Contains("test-token1"))
{
name = "projects/fir-adminintegrationtests/messages/8580920590356323124";
}
else
{
name = "projects/fir-adminintegrationtests/messages/5903525881088369386";
}

return new FirebaseMessagingClient.SingleMessageResponse()
{
Name = name,
};
},
};
var factory = new MockHttpClientFactory(handler);
var client = this.CreateMessagingClient(factory);
var message1 = new Message()
{
Token = "test-token1",
};
var message2 = new Message()
{
Token = "test-token2",
};

var response = await client.SendEachAsync(new[] { message1, message2 });

Assert.Equal(2, response.SuccessCount);
Assert.Equal("projects/fir-adminintegrationtests/messages/8580920590356323124", response.Responses[0].MessageId);
Assert.Equal("projects/fir-adminintegrationtests/messages/5903525881088369386", response.Responses[1].MessageId);
Assert.Equal(2, handler.Calls);
}

[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-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.InternalServerError;
}
},
};
var factory = new MockHttpClientFactory(handler);
var client = this.CreateMessagingClient(factory);
var message1 = new Message()
{
Token = "test-token1",
};
var message2 = new Message()
{
Token = "test-token2",
};

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);
}

[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-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.InternalServerError;
}
},
};
var factory = new MockHttpClientFactory(handler);
var client = this.CreateMessagingClient(factory);
var message1 = new Message()
{
Token = "test-token1",
};
var message2 = new Message()
{
Token = "test-token2",
};

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);
}

[Fact]
public async Task SendEachAsyncNullList()
{
var factory = new MockHttpClientFactory(new MockMessageHandler());
var client = this.CreateMessagingClient(factory);

await Assert.ThrowsAsync<ArgumentNullException>(() => client.SendEachAsync(null));
}

[Fact]
public async Task SendEachAsyncWithNoMessages()
{
var factory = new MockHttpClientFactory(new MockMessageHandler());
var client = this.CreateMessagingClient(factory);
await Assert.ThrowsAsync<ArgumentException>(() => client.SendEachAsync(Enumerable.Empty<Message>()));
}

[Fact]
public async Task SendEachAsyncWithTooManyMessages()
{
var factory = new MockHttpClientFactory(new MockMessageHandler());
var client = this.CreateMessagingClient(factory);
var messages = Enumerable.Range(0, 501).Select(_ => new Message { Topic = "test-topic" });
await Assert.ThrowsAsync<ArgumentException>(() => client.SendEachAsync(messages));
}

[Fact]
public async Task SendAllAsync()
{
Expand Down
24 changes: 24 additions & 0 deletions FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public MockMessageHandler()

public delegate void SetHeaders(HttpResponseHeaders respHeaders, HttpContentHeaders contentHeaders);

public delegate object GetResponse(IncomingRequest request);

public delegate HttpStatusCode GetStatusCode(IncomingRequest request);

/// <summary>
/// Gets the list of request bodies processed by this handler.
/// </summary>
Expand Down Expand Up @@ -89,6 +93,16 @@ public HttpRequestHeaders LastRequestHeaders
/// </summary>
public SetHeaders ApplyHeaders { get; set; }

/// <summary>
/// Gets or sets the function for generating the response based on the incoming request.
/// </summary>
public GetResponse GenerateResponse { get; set; }

/// <summary>
/// Gets or sets the function for generating the status code based on the incoming request.
/// </summary>
public GetStatusCode GenerateStatusCode { get; set; }

protected override async Task<HttpResponseMessage> DoSendAsync(
HttpRequestMessage request, int count, CancellationToken cancellationToken)
{
Expand All @@ -102,6 +116,16 @@ protected override async Task<HttpResponseMessage> DoSendAsync(
return await tcs.Task;
}

if (this.GenerateResponse != null)
{
this.Response = this.GenerateResponse(incomingRequest);
}

if (this.GenerateStatusCode != null)
{
this.StatusCode = this.GenerateStatusCode(incomingRequest);
}

string json;
if (this.Response is byte[])
{
Expand Down
Loading

0 comments on commit 74bd9e5

Please sign in to comment.