Skip to content

Commit

Permalink
Implementing the messaging.SendAll() API (#257)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
hiranya911 committed Jul 18, 2019
1 parent be821cd commit 324b660
Show file tree
Hide file tree
Showing 6 changed files with 939 additions and 82 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Unreleased

- [added] Implemented `messaging.SendAll()` function for sending
up to 100 FCM messages at a time.

# v3.8.1

- [fixed] Fixed a test case that was failing in environments without
Expand Down
117 changes: 112 additions & 5 deletions integration/messaging/messaging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ package messaging

import (
"context"
"errors"
"flag"
"fmt"
"log"
"os"
"regexp"
Expand All @@ -32,6 +34,7 @@ import (
const testRegistrationToken = "fGw0qy4TGgk:APA91bGtWGjuhp4WRhHXgbabIYp1jxEKI08ofj_v1bKhWAGJQ4e3a" +
"rRCWzeTfHaLz83mBnDh0aPWB1AykXAVUUGl2h1wT4XI6XazWpvY7RBUSYfoxtqSWGIm2nvWh2BOP1YG501SsRoE"

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

// Enable API before testing
Expand Down Expand Up @@ -90,16 +93,106 @@ func TestSend(t *testing.T) {
if err != nil {
t.Fatal(err)
}
const pattern = "^projects/.*/messages/.*$"
if !regexp.MustCompile(pattern).MatchString(name) {
t.Errorf("Send() = %q; want = %q", name, pattern)
if !messageIDPattern.MatchString(name) {
t.Errorf("Send() = %q; want = %q", name, messageIDPattern.String())
}
}

func TestSendInvalidToken(t *testing.T) {
msg := &messaging.Message{Token: "INVALID_TOKEN"}
if _, err := client.Send(context.Background(), msg); err == nil {
t.Errorf("Send() = nil; want error")
if _, err := client.Send(context.Background(), msg); err == nil || !messaging.IsInvalidArgument(err) {
t.Errorf("Send() = %v; want InvalidArgumentError", err)
}
}

func TestSendAll(t *testing.T) {
messages := []*messaging.Message{
{
Notification: &messaging.Notification{
Title: "Title 1",
Body: "Body 1",
},
Topic: "foo-bar",
},
{
Notification: &messaging.Notification{
Title: "Title 2",
Body: "Body 2",
},
Topic: "foo-bar",
},
{
Notification: &messaging.Notification{
Title: "Title 3",
Body: "Body 3",
},
Token: "INVALID_TOKEN",
},
}

br, err := client.SendAll(context.Background(), messages)
if err != nil {
t.Fatal(err)
}

if len(br.Responses) != 3 {
t.Errorf("len(Responses) = %d; want = 3", len(br.Responses))
}
if br.SuccessCount != 2 {
t.Errorf("SuccessCount = %d; want = 2", br.SuccessCount)
}
if br.FailureCount != 1 {
t.Errorf("FailureCount = %d; want = 1", br.FailureCount)
}

for i := 0; i < 2; i++ {
sr := br.Responses[i]
if err := checkSuccessfulSendResponse(sr); err != nil {
t.Errorf("Responses[%d]: %v", i, err)
}
}

sr := br.Responses[2]
if sr.Success {
t.Errorf("Responses[2]: Success = true; want = false")
}
if sr.MessageID != "" {
t.Errorf("Responses[2]: MessageID = %q; want = %q", sr.MessageID, "")
}
if sr.Error == nil || !messaging.IsInvalidArgument(sr.Error) {
t.Errorf("Responses[2]: Error = %v; want = InvalidArgumentError", sr.Error)
}
}

func TestSendHundred(t *testing.T) {
var messages []*messaging.Message
for i := 0; i < 100; i++ {
m := &messaging.Message{
Topic: fmt.Sprintf("foo-bar-%d", i%10),
}
messages = append(messages, m)
}

br, err := client.SendAll(context.Background(), messages)
if err != nil {
t.Fatal(err)
}

if len(br.Responses) != 100 {
t.Errorf("len(Responses) = %d; want = 100", len(br.Responses))
}
if br.SuccessCount != 100 {
t.Errorf("SuccessCount = %d; want = 100", br.SuccessCount)
}
if br.FailureCount != 0 {
t.Errorf("FailureCount = %d; want = 0", br.FailureCount)
}

for i := 0; i < 100; i++ {
sr := br.Responses[i]
if err := checkSuccessfulSendResponse(sr); err != nil {
t.Errorf("Responses[%d]: %v", i, err)
}
}
}

Expand All @@ -122,3 +215,17 @@ func TestUnsubscribe(t *testing.T) {
t.Errorf("UnsubscribeFromTopic() = %v; want total 1", tmr)
}
}

func checkSuccessfulSendResponse(sr *messaging.SendResponse) error {
if !sr.Success {
return errors.New("Success = false; want = true")
}
if !messageIDPattern.MatchString(sr.MessageID) {
return fmt.Errorf("MessageID = %q; want = %q", sr.MessageID, messageIDPattern.String())
}
if sr.Error != nil {
return fmt.Errorf("Error = %v; want = nil", sr.Error)
}

return nil
}
37 changes: 24 additions & 13 deletions messaging/messaging.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ import (

const (
messagingEndpoint = "https://fcm.googleapis.com/v1"
batchEndpoint = "https://fcm.googleapis.com/batch"
iidEndpoint = "https://iid.googleapis.com"
iidSubscribe = "iid/v1:batchAdd"
iidUnsubscribe = "iid/v1:batchRemove"

firebaseClientHeader = "X-Firebase-Client"
apiFormatVersionHeader = "X-GOOG-API-FORMAT-VERSION"
apiFormatVersion = "2"

internalError = "internal-error"
invalidAPNSCredentials = "invalid-apns-credentials"
invalidArgument = "invalid-argument"
Expand Down Expand Up @@ -122,11 +127,12 @@ var (

// Client is the interface for the Firebase Cloud Messaging (FCM) service.
type Client struct {
fcmEndpoint string // to enable testing against arbitrary endpoints
iidEndpoint string // to enable testing against arbitrary endpoints
client *internal.HTTPClient
project string
version string
fcmEndpoint string // to enable testing against arbitrary endpoints
batchEndpoint string // to enable testing against arbitrary endpoints
iidEndpoint string // to enable testing against arbitrary endpoints
client *internal.HTTPClient
project string
version string
}

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

return &Client{
fcmEndpoint: messagingEndpoint,
iidEndpoint: iidEndpoint,
client: hc,
project: c.ProjectID,
version: "fire-admin-go/" + c.Version,
fcmEndpoint: messagingEndpoint,
batchEndpoint: batchEndpoint,
iidEndpoint: iidEndpoint,
client: hc,
project: c.ProjectID,
version: "fire-admin-go/" + c.Version,
}, nil
}

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

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

return "", handleFCMError(resp)
}

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

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

0 comments on commit 324b660

Please sign in to comment.