Skip to content

Commit 74bd9e5

Browse files
authored
feat(fcm): Add SendEachAsync and SendEachForMulticastAsync for FCM 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
1 parent 0870aa6 commit 74bd9e5

File tree

5 files changed

+544
-4
lines changed

5 files changed

+544
-4
lines changed

FirebaseAdmin/FirebaseAdmin.IntegrationTests/FirebaseMessagingTest.cs

+72
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,78 @@ public async Task Send()
5252
Assert.Matches(new Regex("^projects/.*/messages/.*$"), id);
5353
}
5454

55+
[Fact]
56+
public async Task SendEach()
57+
{
58+
var message1 = new Message()
59+
{
60+
Topic = "foo-bar",
61+
Notification = new Notification()
62+
{
63+
Title = "Title",
64+
Body = "Body",
65+
ImageUrl = "https://example.com/image.png",
66+
},
67+
Android = new AndroidConfig()
68+
{
69+
Priority = Priority.Normal,
70+
TimeToLive = TimeSpan.FromHours(1),
71+
RestrictedPackageName = "com.google.firebase.testing",
72+
},
73+
};
74+
var message2 = new Message()
75+
{
76+
Topic = "fiz-buz",
77+
Notification = new Notification()
78+
{
79+
Title = "Title",
80+
Body = "Body",
81+
},
82+
Android = new AndroidConfig()
83+
{
84+
Priority = Priority.Normal,
85+
TimeToLive = TimeSpan.FromHours(1),
86+
RestrictedPackageName = "com.google.firebase.testing",
87+
},
88+
};
89+
var response = await FirebaseMessaging.DefaultInstance.SendEachAsync(new[] { message1, message2 }, dryRun: true);
90+
Assert.NotNull(response);
91+
Assert.Equal(2, response.SuccessCount);
92+
Assert.True(!string.IsNullOrEmpty(response.Responses[0].MessageId));
93+
Assert.Matches(new Regex("^projects/.*/messages/.*$"), response.Responses[0].MessageId);
94+
Assert.True(!string.IsNullOrEmpty(response.Responses[1].MessageId));
95+
Assert.Matches(new Regex("^projects/.*/messages/.*$"), response.Responses[1].MessageId);
96+
}
97+
98+
[Fact]
99+
public async Task SendEachForMulticast()
100+
{
101+
var multicastMessage = new MulticastMessage
102+
{
103+
Notification = new Notification()
104+
{
105+
Title = "Title",
106+
Body = "Body",
107+
},
108+
Android = new AndroidConfig()
109+
{
110+
Priority = Priority.Normal,
111+
TimeToLive = TimeSpan.FromHours(1),
112+
RestrictedPackageName = "com.google.firebase.testing",
113+
},
114+
Tokens = new[]
115+
{
116+
"token1",
117+
"token2",
118+
},
119+
};
120+
var response = await FirebaseMessaging.DefaultInstance.SendEachForMulticastAsync(multicastMessage, dryRun: true);
121+
Assert.NotNull(response);
122+
Assert.Equal(2, response.FailureCount);
123+
Assert.NotNull(response.Responses[0].Exception);
124+
Assert.NotNull(response.Responses[1].Exception);
125+
}
126+
55127
[Fact]
56128
public async Task SendAll()
57129
{

FirebaseAdmin/FirebaseAdmin.Tests/Messaging/FirebaseMessagingClientTest.cs

+206
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,212 @@ public async Task SendDryRunAsync()
133133
this.CheckHeaders(handler.LastRequestHeaders);
134134
}
135135

136+
[Fact]
137+
public async Task SendEachAsync()
138+
{
139+
var handler = new MockMessageHandler()
140+
{
141+
GenerateResponse = (incomingRequest) =>
142+
{
143+
string name;
144+
if (incomingRequest.Body.Contains("test-token1"))
145+
{
146+
name = "projects/fir-adminintegrationtests/messages/8580920590356323124";
147+
}
148+
else
149+
{
150+
name = "projects/fir-adminintegrationtests/messages/5903525881088369386";
151+
}
152+
153+
return new FirebaseMessagingClient.SingleMessageResponse()
154+
{
155+
Name = name,
156+
};
157+
},
158+
};
159+
var factory = new MockHttpClientFactory(handler);
160+
var client = this.CreateMessagingClient(factory);
161+
var message1 = new Message()
162+
{
163+
Token = "test-token1",
164+
};
165+
var message2 = new Message()
166+
{
167+
Token = "test-token2",
168+
};
169+
170+
var response = await client.SendEachAsync(new[] { message1, message2 });
171+
172+
Assert.Equal(2, response.SuccessCount);
173+
Assert.Equal("projects/fir-adminintegrationtests/messages/8580920590356323124", response.Responses[0].MessageId);
174+
Assert.Equal("projects/fir-adminintegrationtests/messages/5903525881088369386", response.Responses[1].MessageId);
175+
Assert.Equal(2, handler.Calls);
176+
}
177+
178+
[Fact]
179+
public async Task SendEachAsyncWithError()
180+
{
181+
// Return a success for `message1` and an error for `message2`
182+
var handler = new MockMessageHandler()
183+
{
184+
GenerateResponse = (incomingRequest) =>
185+
{
186+
string name;
187+
if (incomingRequest.Body.Contains("test-token1"))
188+
{
189+
name = "projects/fir-adminintegrationtests/messages/8580920590356323124";
190+
return new FirebaseMessagingClient.SingleMessageResponse()
191+
{
192+
Name = name,
193+
};
194+
}
195+
else
196+
{
197+
return @"{
198+
""error"": {
199+
""status"": ""INVALID_ARGUMENT"",
200+
""message"": ""The registration token is not a valid FCM registration token"",
201+
""details"": [
202+
{
203+
""@type"": ""type.googleapis.com/google.firebase.fcm.v1.FcmError"",
204+
""errorCode"": ""UNREGISTERED""
205+
}
206+
]
207+
}
208+
}";
209+
}
210+
},
211+
GenerateStatusCode = (incomingRequest) =>
212+
{
213+
if (incomingRequest.Body.Contains("test-token1"))
214+
{
215+
return HttpStatusCode.OK;
216+
}
217+
else
218+
{
219+
return HttpStatusCode.InternalServerError;
220+
}
221+
},
222+
};
223+
var factory = new MockHttpClientFactory(handler);
224+
var client = this.CreateMessagingClient(factory);
225+
var message1 = new Message()
226+
{
227+
Token = "test-token1",
228+
};
229+
var message2 = new Message()
230+
{
231+
Token = "test-token2",
232+
};
233+
234+
var response = await client.SendEachAsync(new[] { message1, message2 });
235+
236+
Assert.Equal(1, response.SuccessCount);
237+
Assert.Equal(1, response.FailureCount);
238+
Assert.Equal("projects/fir-adminintegrationtests/messages/8580920590356323124", response.Responses[0].MessageId);
239+
240+
var exception = response.Responses[1].Exception;
241+
Assert.NotNull(exception);
242+
Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode);
243+
Assert.Equal("The registration token is not a valid FCM registration token", exception.Message);
244+
Assert.Equal(MessagingErrorCode.Unregistered, exception.MessagingErrorCode);
245+
Assert.NotNull(exception.HttpResponse);
246+
247+
Assert.Equal(2, handler.Calls);
248+
}
249+
250+
[Fact]
251+
public async Task SendEachAsyncWithErrorNoDetail()
252+
{
253+
// Return a success for `message1` and an error for `message2`
254+
var handler = new MockMessageHandler()
255+
{
256+
GenerateResponse = (incomingRequest) =>
257+
{
258+
string name;
259+
if (incomingRequest.Body.Contains("test-token1"))
260+
{
261+
name = "projects/fir-adminintegrationtests/messages/8580920590356323124";
262+
return new FirebaseMessagingClient.SingleMessageResponse()
263+
{
264+
Name = name,
265+
};
266+
}
267+
else
268+
{
269+
return @"{
270+
""error"": {
271+
""status"": ""INVALID_ARGUMENT"",
272+
""message"": ""The registration token is not a valid FCM registration token"",
273+
}
274+
}";
275+
}
276+
},
277+
GenerateStatusCode = (incomingRequest) =>
278+
{
279+
if (incomingRequest.Body.Contains("test-token1"))
280+
{
281+
return HttpStatusCode.OK;
282+
}
283+
else
284+
{
285+
return HttpStatusCode.InternalServerError;
286+
}
287+
},
288+
};
289+
var factory = new MockHttpClientFactory(handler);
290+
var client = this.CreateMessagingClient(factory);
291+
var message1 = new Message()
292+
{
293+
Token = "test-token1",
294+
};
295+
var message2 = new Message()
296+
{
297+
Token = "test-token2",
298+
};
299+
300+
var response = await client.SendEachAsync(new[] { message1, message2 });
301+
302+
Assert.Equal(1, response.SuccessCount);
303+
Assert.Equal(1, response.FailureCount);
304+
Assert.Equal("projects/fir-adminintegrationtests/messages/8580920590356323124", response.Responses[0].MessageId);
305+
306+
var exception = response.Responses[1].Exception;
307+
Assert.NotNull(exception);
308+
Assert.Equal(ErrorCode.InvalidArgument, exception.ErrorCode);
309+
Assert.Equal("The registration token is not a valid FCM registration token", exception.Message);
310+
Assert.Null(exception.MessagingErrorCode);
311+
Assert.NotNull(exception.HttpResponse);
312+
313+
Assert.Equal(2, handler.Calls);
314+
}
315+
316+
[Fact]
317+
public async Task SendEachAsyncNullList()
318+
{
319+
var factory = new MockHttpClientFactory(new MockMessageHandler());
320+
var client = this.CreateMessagingClient(factory);
321+
322+
await Assert.ThrowsAsync<ArgumentNullException>(() => client.SendEachAsync(null));
323+
}
324+
325+
[Fact]
326+
public async Task SendEachAsyncWithNoMessages()
327+
{
328+
var factory = new MockHttpClientFactory(new MockMessageHandler());
329+
var client = this.CreateMessagingClient(factory);
330+
await Assert.ThrowsAsync<ArgumentException>(() => client.SendEachAsync(Enumerable.Empty<Message>()));
331+
}
332+
333+
[Fact]
334+
public async Task SendEachAsyncWithTooManyMessages()
335+
{
336+
var factory = new MockHttpClientFactory(new MockMessageHandler());
337+
var client = this.CreateMessagingClient(factory);
338+
var messages = Enumerable.Range(0, 501).Select(_ => new Message { Topic = "test-topic" });
339+
await Assert.ThrowsAsync<ArgumentException>(() => client.SendEachAsync(messages));
340+
}
341+
136342
[Fact]
137343
public async Task SendAllAsync()
138344
{

FirebaseAdmin/FirebaseAdmin.Tests/MockMessageHandler.cs

+24
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ public MockMessageHandler()
4040

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

43+
public delegate object GetResponse(IncomingRequest request);
44+
45+
public delegate HttpStatusCode GetStatusCode(IncomingRequest request);
46+
4347
/// <summary>
4448
/// Gets the list of request bodies processed by this handler.
4549
/// </summary>
@@ -89,6 +93,16 @@ public HttpRequestHeaders LastRequestHeaders
8993
/// </summary>
9094
public SetHeaders ApplyHeaders { get; set; }
9195

96+
/// <summary>
97+
/// Gets or sets the function for generating the response based on the incoming request.
98+
/// </summary>
99+
public GetResponse GenerateResponse { get; set; }
100+
101+
/// <summary>
102+
/// Gets or sets the function for generating the status code based on the incoming request.
103+
/// </summary>
104+
public GetStatusCode GenerateStatusCode { get; set; }
105+
92106
protected override async Task<HttpResponseMessage> DoSendAsync(
93107
HttpRequestMessage request, int count, CancellationToken cancellationToken)
94108
{
@@ -102,6 +116,16 @@ protected override async Task<HttpResponseMessage> DoSendAsync(
102116
return await tcs.Task;
103117
}
104118

119+
if (this.GenerateResponse != null)
120+
{
121+
this.Response = this.GenerateResponse(incomingRequest);
122+
}
123+
124+
if (this.GenerateStatusCode != null)
125+
{
126+
this.StatusCode = this.GenerateStatusCode(incomingRequest);
127+
}
128+
105129
string json;
106130
if (this.Response is byte[])
107131
{

0 commit comments

Comments
 (0)