Skip to content

Commit 324b660

Browse files
authored
Implementing the messaging.SendAll() API (#257)
* Basic batch messaging support * Cleaned up the response parsing logic * Added more tests and completed the basic impl * Using core http package for req serialization * Added more tests; updated documentation * Added constants for FCM API format version header
1 parent be821cd commit 324b660

File tree

6 files changed

+939
-82
lines changed

6 files changed

+939
-82
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Unreleased
22

3+
- [added] Implemented `messaging.SendAll()` function for sending
4+
up to 100 FCM messages at a time.
5+
36
# v3.8.1
47

58
- [fixed] Fixed a test case that was failing in environments without

integration/messaging/messaging_test.go

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ package messaging
1616

1717
import (
1818
"context"
19+
"errors"
1920
"flag"
21+
"fmt"
2022
"log"
2123
"os"
2224
"regexp"
@@ -32,6 +34,7 @@ import (
3234
const testRegistrationToken = "fGw0qy4TGgk:APA91bGtWGjuhp4WRhHXgbabIYp1jxEKI08ofj_v1bKhWAGJQ4e3a" +
3335
"rRCWzeTfHaLz83mBnDh0aPWB1AykXAVUUGl2h1wT4XI6XazWpvY7RBUSYfoxtqSWGIm2nvWh2BOP1YG501SsRoE"
3436

37+
var messageIDPattern = regexp.MustCompile("^projects/.*/messages/.*$")
3538
var client *messaging.Client
3639

3740
// Enable API before testing
@@ -90,16 +93,106 @@ func TestSend(t *testing.T) {
9093
if err != nil {
9194
t.Fatal(err)
9295
}
93-
const pattern = "^projects/.*/messages/.*$"
94-
if !regexp.MustCompile(pattern).MatchString(name) {
95-
t.Errorf("Send() = %q; want = %q", name, pattern)
96+
if !messageIDPattern.MatchString(name) {
97+
t.Errorf("Send() = %q; want = %q", name, messageIDPattern.String())
9698
}
9799
}
98100

99101
func TestSendInvalidToken(t *testing.T) {
100102
msg := &messaging.Message{Token: "INVALID_TOKEN"}
101-
if _, err := client.Send(context.Background(), msg); err == nil {
102-
t.Errorf("Send() = nil; want error")
103+
if _, err := client.Send(context.Background(), msg); err == nil || !messaging.IsInvalidArgument(err) {
104+
t.Errorf("Send() = %v; want InvalidArgumentError", err)
105+
}
106+
}
107+
108+
func TestSendAll(t *testing.T) {
109+
messages := []*messaging.Message{
110+
{
111+
Notification: &messaging.Notification{
112+
Title: "Title 1",
113+
Body: "Body 1",
114+
},
115+
Topic: "foo-bar",
116+
},
117+
{
118+
Notification: &messaging.Notification{
119+
Title: "Title 2",
120+
Body: "Body 2",
121+
},
122+
Topic: "foo-bar",
123+
},
124+
{
125+
Notification: &messaging.Notification{
126+
Title: "Title 3",
127+
Body: "Body 3",
128+
},
129+
Token: "INVALID_TOKEN",
130+
},
131+
}
132+
133+
br, err := client.SendAll(context.Background(), messages)
134+
if err != nil {
135+
t.Fatal(err)
136+
}
137+
138+
if len(br.Responses) != 3 {
139+
t.Errorf("len(Responses) = %d; want = 3", len(br.Responses))
140+
}
141+
if br.SuccessCount != 2 {
142+
t.Errorf("SuccessCount = %d; want = 2", br.SuccessCount)
143+
}
144+
if br.FailureCount != 1 {
145+
t.Errorf("FailureCount = %d; want = 1", br.FailureCount)
146+
}
147+
148+
for i := 0; i < 2; i++ {
149+
sr := br.Responses[i]
150+
if err := checkSuccessfulSendResponse(sr); err != nil {
151+
t.Errorf("Responses[%d]: %v", i, err)
152+
}
153+
}
154+
155+
sr := br.Responses[2]
156+
if sr.Success {
157+
t.Errorf("Responses[2]: Success = true; want = false")
158+
}
159+
if sr.MessageID != "" {
160+
t.Errorf("Responses[2]: MessageID = %q; want = %q", sr.MessageID, "")
161+
}
162+
if sr.Error == nil || !messaging.IsInvalidArgument(sr.Error) {
163+
t.Errorf("Responses[2]: Error = %v; want = InvalidArgumentError", sr.Error)
164+
}
165+
}
166+
167+
func TestSendHundred(t *testing.T) {
168+
var messages []*messaging.Message
169+
for i := 0; i < 100; i++ {
170+
m := &messaging.Message{
171+
Topic: fmt.Sprintf("foo-bar-%d", i%10),
172+
}
173+
messages = append(messages, m)
174+
}
175+
176+
br, err := client.SendAll(context.Background(), messages)
177+
if err != nil {
178+
t.Fatal(err)
179+
}
180+
181+
if len(br.Responses) != 100 {
182+
t.Errorf("len(Responses) = %d; want = 100", len(br.Responses))
183+
}
184+
if br.SuccessCount != 100 {
185+
t.Errorf("SuccessCount = %d; want = 100", br.SuccessCount)
186+
}
187+
if br.FailureCount != 0 {
188+
t.Errorf("FailureCount = %d; want = 0", br.FailureCount)
189+
}
190+
191+
for i := 0; i < 100; i++ {
192+
sr := br.Responses[i]
193+
if err := checkSuccessfulSendResponse(sr); err != nil {
194+
t.Errorf("Responses[%d]: %v", i, err)
195+
}
103196
}
104197
}
105198

@@ -122,3 +215,17 @@ func TestUnsubscribe(t *testing.T) {
122215
t.Errorf("UnsubscribeFromTopic() = %v; want total 1", tmr)
123216
}
124217
}
218+
219+
func checkSuccessfulSendResponse(sr *messaging.SendResponse) error {
220+
if !sr.Success {
221+
return errors.New("Success = false; want = true")
222+
}
223+
if !messageIDPattern.MatchString(sr.MessageID) {
224+
return fmt.Errorf("MessageID = %q; want = %q", sr.MessageID, messageIDPattern.String())
225+
}
226+
if sr.Error != nil {
227+
return fmt.Errorf("Error = %v; want = nil", sr.Error)
228+
}
229+
230+
return nil
231+
}

messaging/messaging.go

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,15 @@ import (
3232

3333
const (
3434
messagingEndpoint = "https://fcm.googleapis.com/v1"
35+
batchEndpoint = "https://fcm.googleapis.com/batch"
3536
iidEndpoint = "https://iid.googleapis.com"
3637
iidSubscribe = "iid/v1:batchAdd"
3738
iidUnsubscribe = "iid/v1:batchRemove"
3839

40+
firebaseClientHeader = "X-Firebase-Client"
41+
apiFormatVersionHeader = "X-GOOG-API-FORMAT-VERSION"
42+
apiFormatVersion = "2"
43+
3944
internalError = "internal-error"
4045
invalidAPNSCredentials = "invalid-apns-credentials"
4146
invalidArgument = "invalid-argument"
@@ -122,11 +127,12 @@ var (
122127

123128
// Client is the interface for the Firebase Cloud Messaging (FCM) service.
124129
type Client struct {
125-
fcmEndpoint string // to enable testing against arbitrary endpoints
126-
iidEndpoint string // to enable testing against arbitrary endpoints
127-
client *internal.HTTPClient
128-
project string
129-
version string
130+
fcmEndpoint string // to enable testing against arbitrary endpoints
131+
batchEndpoint string // to enable testing against arbitrary endpoints
132+
iidEndpoint string // to enable testing against arbitrary endpoints
133+
client *internal.HTTPClient
134+
project string
135+
version string
130136
}
131137

132138
// Message to be sent via Firebase Cloud Messaging.
@@ -658,11 +664,12 @@ func NewClient(ctx context.Context, c *internal.MessagingConfig) (*Client, error
658664
}
659665

660666
return &Client{
661-
fcmEndpoint: messagingEndpoint,
662-
iidEndpoint: iidEndpoint,
663-
client: hc,
664-
project: c.ProjectID,
665-
version: "fire-admin-go/" + c.Version,
667+
fcmEndpoint: messagingEndpoint,
668+
batchEndpoint: batchEndpoint,
669+
iidEndpoint: iidEndpoint,
670+
client: hc,
671+
project: c.ProjectID,
672+
version: "fire-admin-go/" + c.Version,
666673
}, nil
667674
}
668675

@@ -808,8 +815,8 @@ func (c *Client) makeSendRequest(ctx context.Context, req *fcmRequest) (string,
808815
URL: fmt.Sprintf("%s/projects/%s/messages:send", c.fcmEndpoint, c.project),
809816
Body: internal.NewJSONEntity(req),
810817
Opts: []internal.HTTPOption{
811-
internal.WithHeader("X-GOOG-API-FORMAT-VERSION", "2"),
812-
internal.WithHeader("X-FIREBASE-CLIENT", c.version),
818+
internal.WithHeader(apiFormatVersionHeader, apiFormatVersion),
819+
internal.WithHeader(firebaseClientHeader, c.version),
813820
},
814821
}
815822

@@ -824,6 +831,10 @@ func (c *Client) makeSendRequest(ctx context.Context, req *fcmRequest) (string,
824831
return result.Name, err
825832
}
826833

834+
return "", handleFCMError(resp)
835+
}
836+
837+
func handleFCMError(resp *internal.Response) error {
827838
var fe fcmError
828839
json.Unmarshal(resp.Body, &fe) // ignore any json parse errors at this level
829840
var serverCode string
@@ -848,7 +859,7 @@ func (c *Client) makeSendRequest(ctx context.Context, req *fcmRequest) (string,
848859
if fe.Error.Message != "" {
849860
msg += "; details: " + fe.Error.Message
850861
}
851-
return "", internal.Errorf(clientCode, "http error status: %d; reason: %s", resp.Status, msg)
862+
return internal.Errorf(clientCode, "http error status: %d; reason: %s", resp.Status, msg)
852863
}
853864

854865
func (c *Client) makeTopicManagementRequest(ctx context.Context, req *iidRequest) (*TopicManagementResponse, error) {

0 commit comments

Comments
 (0)