diff --git a/integration/messaging/messaging_test.go b/integration/messaging/messaging_test.go index 4552988a..7c123b7f 100644 --- a/integration/messaging/messaging_test.go +++ b/integration/messaging/messaging_test.go @@ -105,6 +105,13 @@ func TestSendInvalidToken(t *testing.T) { } } +func TestSendInvalidFid(t *testing.T) { + msg := &messaging.Message{Fid: "INVALID_FID"} + if _, err := client.Send(context.Background(), msg); err == nil || !messaging.IsUnregistered(err) { + t.Errorf("Send() = %v; want UnregisteredError", err) + } +} + func TestSendEach(t *testing.T) { messages := []*messaging.Message{ { @@ -229,6 +236,44 @@ func TestSendEachForMulticast(t *testing.T) { } } +func TestSendEachForMulticastFids(t *testing.T) { + message := &messaging.MulticastMessage{ + Notification: &messaging.Notification{ + Title: "title", + Body: "body", + }, + Fids: []string{"INVALID_FID", "ANOTHER_INVALID_FID"}, + } + + br, err := client.SendEachForMulticastDryRun(context.Background(), message) + if err != nil { + t.Fatal(err) + } + + if len(br.Responses) != 2 { + t.Errorf("len(Responses) = %d; want = 2", len(br.Responses)) + } + if br.SuccessCount != 0 { + t.Errorf("SuccessCount = %d; want = 0", br.SuccessCount) + } + if br.FailureCount != 2 { + t.Errorf("FailureCount = %d; want = 2", br.FailureCount) + } + + for i := 0; i < 2; i++ { + sr := br.Responses[i] + if sr.Success { + t.Errorf("Responses[%d]: Success = true; want = false", i) + } + if sr.MessageID != "" { + t.Errorf("Responses[%d]: MessageID = %q; want = %q", i, sr.MessageID, "") + } + if sr.Error == nil || !messaging.IsUnregistered(sr.Error) { + t.Errorf("Responses[%d]: Error = %v; want = UnregisteredError", i, sr.Error) + } + } +} + func TestSendAll(t *testing.T) { t.Skip("Skipping integration tests for deprecated sendAll() API") messages := []*messaging.Message{ diff --git a/messaging/messaging.go b/messaging/messaging.go index 67b94f66..fb9ef34a 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -58,7 +58,7 @@ var ( // Message to be sent via Firebase Cloud Messaging. // // Message contains payload data, recipient information and platform-specific configuration -// options. A Message must specify exactly one of Token, Topic or Condition fields. Apart from +// options. A Message must specify exactly one of Fid, Token, Topic or Condition fields. Apart from // that a Message may specify any combination of Data, Notification, Android, Webpush and APNS // fields. See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages for more // details on how the backend FCM servers handle different message parameters. @@ -69,9 +69,11 @@ type Message struct { Webpush *WebpushConfig `json:"webpush,omitempty"` APNS *APNSConfig `json:"apns,omitempty"` FCMOptions *FCMOptions `json:"fcm_options,omitempty"` - Token string `json:"token,omitempty"` - Topic string `json:"-"` - Condition string `json:"condition,omitempty"` + // Deprecated: Use Fid instead. + Token string `json:"token,omitempty"` + Topic string `json:"-"` + Condition string `json:"condition,omitempty"` + Fid string `json:"fid,omitempty"` } // MarshalJSON marshals a Message into JSON (for internal use only). @@ -958,7 +960,7 @@ func newFCMClient(hc *http.Client, conf *internal.MessagingConfig, messagingEndp // Send sends a Message to Firebase Cloud Messaging. // -// The Message must specify exactly one of Token, Topic and Condition fields. FCM will +// The Message must specify exactly one of Fid, Token, Topic or Condition fields. FCM will // customize the message for each target platform based on the arguments specified in the // Message. func (c *fcmClient) Send(ctx context.Context, message *Message) (string, error) { @@ -1054,8 +1056,8 @@ func IsRegistrationTokenNotRegistered(err error) bool { return IsUnregistered(err) } -// IsUnregistered checks if the given error was due to a registration token that -// became invalid. +// IsUnregistered checks if the given error was due to a registration token or +// installation ID (FID) that was unregistered or became invalid. func IsUnregistered(err error) bool { return hasMessagingErrorCode(err, unregistered) } diff --git a/messaging/messaging_batch.go b/messaging/messaging_batch.go index 03129587..d1e63288 100644 --- a/messaging/messaging_batch.go +++ b/messaging/messaging_batch.go @@ -37,9 +37,11 @@ const multipartBoundary = "__END_OF_PART__" // MulticastMessage represents a message that can be sent to multiple devices via Firebase Cloud // Messaging (FCM). // -// It contains payload information as well as the list of device registration tokens to which the -// message should be sent. A single MulticastMessage may contain up to 500 registration tokens. +// It contains payload information as well as the list of device registration tokens and/or +// Firebase Installation IDs (FIDs) to which the message should be sent. A single +// MulticastMessage may contain up to 500 registration tokens and FIDs combined. type MulticastMessage struct { + // Deprecated: Use Fids instead. Tokens []string Data map[string]string Notification *Notification @@ -47,17 +49,19 @@ type MulticastMessage struct { Webpush *WebpushConfig APNS *APNSConfig FCMOptions *FCMOptions + Fids []string } func (mm *MulticastMessage) toMessages() ([]*Message, error) { - if len(mm.Tokens) == 0 { - return nil, errors.New("tokens must not be nil or empty") + if len(mm.Tokens) == 0 && len(mm.Fids) == 0 { + return nil, errors.New("either tokens or fids must be specified") } - if len(mm.Tokens) > maxMessages { - return nil, fmt.Errorf("tokens must not contain more than %d elements", maxMessages) + total := len(mm.Tokens) + len(mm.Fids) + if total > maxMessages { + return nil, fmt.Errorf("total tokens and fids must not exceed %d elements", maxMessages) } - var messages []*Message + messages := make([]*Message, 0, total) for _, token := range mm.Tokens { temp := &Message{ Token: token, @@ -70,6 +74,18 @@ func (mm *MulticastMessage) toMessages() ([]*Message, error) { } messages = append(messages, temp) } + for _, fid := range mm.Fids { + temp := &Message{ + Fid: fid, + Data: mm.Data, + Notification: mm.Notification, + Android: mm.Android, + Webpush: mm.Webpush, + APNS: mm.APNS, + FCMOptions: mm.FCMOptions, + } + messages = append(messages, temp) + } return messages, nil } @@ -117,12 +133,14 @@ func (c *fcmClient) SendEachDryRun(ctx context.Context, messages []*Message) (*B return c.sendEachInBatch(ctx, messages, true) } -// SendEachForMulticast sends the given multicast message to all the FCM registration tokens specified. +// SendEachForMulticast sends the given multicast message to all the specified FCM registration +// tokens and/or Firebase Installation IDs (FIDs). // -// The tokens array in MulticastMessage may contain up to 500 tokens. SendMulticast uses the -// SendEach() function to send the given message to all the target recipients. The -// responses list obtained from the return value corresponds to the order of the input tokens. An error -// from SendEachForMulticast or a BatchResponse with all failures indicates a total failure, meaning +// The tokens and FIDs in MulticastMessage may contain up to 500 elements in total. +// SendEachForMulticast uses the SendEach() function to send the given message. The responses list +// obtained from the return value corresponds to the order of the input targets. If both tokens +// and FIDs are provided, tokens are processed first, followed by FIDs. An error from +// SendEachForMulticast or a BatchResponse with all failures indicates a total failure, meaning // that none of the messages in the list could be sent. Partial failures or no failures are only // indicated by a BatchResponse return value. func (c *fcmClient) SendEachForMulticast(ctx context.Context, message *MulticastMessage) (*BatchResponse, error) { @@ -135,16 +153,17 @@ func (c *fcmClient) SendEachForMulticast(ctx context.Context, message *Multicast } // SendEachForMulticastDryRun sends the given multicast message to all the specified FCM registration -// tokens in the dry run (validation only) mode. +// tokens and/or Firebase Installation IDs (FIDs) in the dry run (validation only) mode. // // This function does not actually deliver any messages to target devices. Instead, it performs all // the SDK-level and backend validations on the messages, and emulates the send operation. // -// The tokens array in MulticastMessage may contain up to 500 tokens. SendEachForMulticastDryRunn uses the -// SendEachDryRun() function to send the given message. The responses list obtained from -// the return value corresponds to the order of the input tokens. An error from SendEachForMulticastDryRun -// or a BatchResponse with all failures indicates a total failure, meaning that of the messages in the -// list could be sent. Partial failures or no failures are only +// The tokens and FIDs in MulticastMessage may contain up to 500 elements in total. +// SendEachForMulticastDryRun uses the SendEachDryRun() function to send the given message. +// The responses list obtained from the return value corresponds to the order of the input targets. +// If both tokens and FIDs are provided, tokens are processed first, followed by FIDs. An error from +// SendEachForMulticastDryRun or a BatchResponse with all failures indicates a total failure, meaning +// that none of the messages in the list could be sent. Partial failures or no failures are only // indicated by a BatchResponse return value. func (c *fcmClient) SendEachForMulticastDryRun(ctx context.Context, message *MulticastMessage) (*BatchResponse, error) { messages, err := toMessages(message) @@ -275,13 +294,16 @@ func (c *fcmClient) SendAllDryRun(ctx context.Context, messages []*Message) (*Ba return c.sendBatch(ctx, messages, true) } -// SendMulticast sends the given multicast message to all the FCM registration tokens specified. +// SendMulticast sends the given multicast message to all the specified FCM registration +// tokens and/or Firebase Installation IDs (FIDs). // -// The tokens array in MulticastMessage may contain up to 500 tokens. SendMulticast uses the -// SendAll() function to send the given message to all the target recipients. The -// responses list obtained from the return value corresponds to the order of the input tokens. An -// error from SendMulticast indicates a total failure, meaning that the message could not be sent -// to any of the recipients. Partial failures are indicated by a BatchResponse return value. +// The tokens and FIDs in MulticastMessage may contain up to 500 elements in total. +// SendMulticast uses the SendAll() function to send the given message to all the target +// recipients. The responses list obtained from the return value corresponds to the order of +// the input targets. If both tokens and FIDs are provided, tokens are processed first, +// followed by FIDs. An error from SendMulticast indicates a total failure, meaning that the +// message could not be sent to any of the recipients. Partial failures are indicated by a +// BatchResponse return value. // // Deprecated: Use SendEachForMulticast instead. func (c *fcmClient) SendMulticast(ctx context.Context, message *MulticastMessage) (*BatchResponse, error) { @@ -294,16 +316,17 @@ func (c *fcmClient) SendMulticast(ctx context.Context, message *MulticastMessage } // SendMulticastDryRun sends the given multicast message to all the specified FCM registration -// tokens in the dry run (validation only) mode. +// tokens and/or Firebase Installation IDs (FIDs) in the dry run (validation only) mode. // // This function does not actually deliver any messages to target devices. Instead, it performs all // the SDK-level and backend validations on the messages, and emulates the send operation. // -// The tokens array in MulticastMessage may contain up to 500 tokens. SendMulticastDryRun uses the -// SendAllDryRun() function to send the given message. The responses list obtained from -// the return value corresponds to the order of the input tokens. An error from SendMulticastDryRun -// indicates a total failure, meaning that none of the messages were sent to FCM for validation. -// Partial failures are indicated by a BatchResponse return value. +// The tokens and FIDs in MulticastMessage may contain up to 500 elements in total. +// SendMulticastDryRun uses the SendAllDryRun() function to send the given message. +// The responses list obtained from the return value corresponds to the order of the input targets. +// If both tokens and FIDs are provided, tokens are processed first, followed by FIDs. An error +// from SendMulticastDryRun indicates a total failure, meaning that none of the messages +// were sent to FCM for validation. Partial failures are indicated by a BatchResponse return value. // // Deprecated: Use SendEachForMulticastDryRun instead. func (c *fcmClient) SendMulticastDryRun(ctx context.Context, message *MulticastMessage) (*BatchResponse, error) { diff --git a/messaging/messaging_batch_test.go b/messaging/messaging_batch_test.go index ed9a1a42..25ac5a11 100644 --- a/messaging/messaging_batch_test.go +++ b/messaging/messaging_batch_test.go @@ -37,6 +37,13 @@ var testMessages = []*Message{{Topic: "topic1"}, {Topic: "topic2"}} var testMulticastMessage = &MulticastMessage{ Tokens: []string{"token1", "token2"}, } +var testFidMulticastMessage = &MulticastMessage{ + Fids: []string{"fid1", "fid2"}, +} +var testMixedMulticastMessage = &MulticastMessage{ + Tokens: []string{"token1"}, + Fids: []string{"fid2"}, +} var testSuccessResponse = []fcmResponse{ {Name: "projects/test-project/messages/1"}, {Name: "projects/test-project/messages/2"}, @@ -642,7 +649,7 @@ func TestSendEachForMulticastEmptyArray(t *testing.T) { t.Fatal(err) } - want := "tokens must not be nil or empty" + want := "either tokens or fids must be specified" mm := &MulticastMessage{} br, err := client.SendEachForMulticast(ctx, mm) if err == nil || err.Error() != want { @@ -657,9 +664,18 @@ func TestSendEachForMulticastEmptyArray(t *testing.T) { if err == nil || err.Error() != want { t.Errorf("SendEachForMulticast(Tokens: []) = (%v, %v); want = (nil, %q)", br, err, want) } + + var fids []string + mm = &MulticastMessage{ + Fids: fids, + } + br, err = client.SendEachForMulticast(ctx, mm) + if err == nil || err.Error() != want { + t.Errorf("SendEachForMulticast(Fids: []) = (%v, %v); want = (nil, %q)", br, err, want) + } } -func TestSendEachForMulticastTooManyTokens(t *testing.T) { +func TestSendEachForMulticastTooManyTargets(t *testing.T) { ctx := context.Background() client, err := NewClient(ctx, testMessagingConfig) if err != nil { @@ -671,12 +687,36 @@ func TestSendEachForMulticastTooManyTokens(t *testing.T) { tokens = append(tokens, fmt.Sprintf("token%d", i)) } - want := "tokens must not contain more than 500 elements" + want := "total tokens and fids must not exceed 500 elements" mm := &MulticastMessage{Tokens: tokens} br, err := client.SendEachForMulticast(ctx, mm) if err == nil || err.Error() != want { t.Errorf("SendEachForMulticast() = (%v, %v); want = (nil, %q)", br, err, want) } + + var fids []string + for i := 0; i < 501; i++ { + fids = append(fids, fmt.Sprintf("fid%d", i)) + } + mm = &MulticastMessage{Fids: fids} + br, err = client.SendEachForMulticast(ctx, mm) + if err == nil || err.Error() != want { + t.Errorf("SendEachForMulticast() = (%v, %v); want = (nil, %q)", br, err, want) + } + + mixedTokens := []string{} + mixedFids := []string{} + for i := 0; i < 250; i++ { + mixedTokens = append(mixedTokens, fmt.Sprintf("token%d", i)) + } + for i := 0; i < 251; i++ { + mixedFids = append(mixedFids, fmt.Sprintf("fid%d", i)) + } + mm = &MulticastMessage{Tokens: mixedTokens, Fids: mixedFids} + br, err = client.SendEachForMulticast(ctx, mm) + if err == nil || err.Error() != want { + t.Errorf("SendEachForMulticast() = (%v, %v); want = (nil, %q)", br, err, want) + } } func TestSendEachForMulticastInvalidMessage(t *testing.T) { @@ -725,6 +765,67 @@ func TestSendEachForMulticast(t *testing.T) { } } +func TestSendEachForMulticastFids(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + req, _ := ioutil.ReadAll(r.Body) + w.Header().Set("Content-Type", "application/json") + for idx, fid := range testFidMulticastMessage.Fids { + if strings.Contains(string(req), fid) { + w.Write([]byte("{ \"name\":\"" + testSuccessResponse[idx].Name + "\" }")) + } + } + })) + defer ts.Close() + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + client.fcmEndpoint = ts.URL + + br, err := client.SendEachForMulticast(ctx, testFidMulticastMessage) + if err != nil { + t.Fatal(err) + } + + if err := checkSuccessfulBatchResponseForSendEach(br, false); err != nil { + t.Errorf("SendEachForMulticast() = %v", err) + } +} + +func TestSendEachForMulticastMixed(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + req, _ := ioutil.ReadAll(r.Body) + w.Header().Set("Content-Type", "application/json") + for idx, token := range testMixedMulticastMessage.Tokens { + if strings.Contains(string(req), token) { + w.Write([]byte("{ \"name\":\"" + testSuccessResponse[idx].Name + "\" }")) + } + } + for idx, fid := range testMixedMulticastMessage.Fids { + if strings.Contains(string(req), fid) { + w.Write([]byte("{ \"name\":\"" + testSuccessResponse[idx+1].Name + "\" }")) + } + } + })) + defer ts.Close() + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + client.fcmEndpoint = ts.URL + + br, err := client.SendEachForMulticast(ctx, testMixedMulticastMessage) + if err != nil { + t.Fatal(err) + } + + if err := checkSuccessfulBatchResponseForSendEach(br, false); err != nil { + t.Errorf("SendEachForMulticast() = %v", err) + } +} + func TestSendEachForMulticastWithCustomEndpoint(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { req, _ := ioutil.ReadAll(r.Body) @@ -1099,7 +1200,7 @@ func TestSendMulticastEmptyArray(t *testing.T) { t.Fatal(err) } - want := "tokens must not be nil or empty" + want := "either tokens or fids must be specified" mm := &MulticastMessage{} br, err := client.SendMulticast(ctx, mm) if err == nil || err.Error() != want { @@ -1114,9 +1215,18 @@ func TestSendMulticastEmptyArray(t *testing.T) { if err == nil || err.Error() != want { t.Errorf("SendMulticast(Tokens: []) = (%v, %v); want = (nil, %q)", br, err, want) } + + var fids []string + mm = &MulticastMessage{ + Fids: fids, + } + br, err = client.SendMulticast(ctx, mm) + if err == nil || err.Error() != want { + t.Errorf("SendMulticast(Fids: []) = (%v, %v); want = (nil, %q)", br, err, want) + } } -func TestSendMulticastTooManyTokens(t *testing.T) { +func TestSendMulticastTooManyTargets(t *testing.T) { ctx := context.Background() client, err := NewClient(ctx, testMessagingConfig) if err != nil { @@ -1128,12 +1238,36 @@ func TestSendMulticastTooManyTokens(t *testing.T) { tokens = append(tokens, fmt.Sprintf("token%d", i)) } - want := "tokens must not contain more than 500 elements" + want := "total tokens and fids must not exceed 500 elements" mm := &MulticastMessage{Tokens: tokens} br, err := client.SendMulticast(ctx, mm) if err == nil || err.Error() != want { t.Errorf("SendMulticast() = (%v, %v); want = (nil, %q)", br, err, want) } + + var fids []string + for i := 0; i < 501; i++ { + fids = append(fids, fmt.Sprintf("fid%d", i)) + } + mm = &MulticastMessage{Fids: fids} + br, err = client.SendMulticast(ctx, mm) + if err == nil || err.Error() != want { + t.Errorf("SendMulticast() = (%v, %v); want = (nil, %q)", br, err, want) + } + + mixedTokens := []string{} + mixedFids := []string{} + for i := 0; i < 250; i++ { + mixedTokens = append(mixedTokens, fmt.Sprintf("token%d", i)) + } + for i := 0; i < 251; i++ { + mixedFids = append(mixedFids, fmt.Sprintf("fid%d", i)) + } + mm = &MulticastMessage{Tokens: mixedTokens, Fids: mixedFids} + br, err = client.SendMulticast(ctx, mm) + if err == nil || err.Error() != want { + t.Errorf("SendMulticast() = (%v, %v); want = (nil, %q)", br, err, want) + } } func TestSendMulticastInvalidMessage(t *testing.T) { diff --git a/messaging/messaging_test.go b/messaging/messaging_test.go index 5ba548af..00d4fee9 100644 --- a/messaging/messaging_test.go +++ b/messaging/messaging_test.go @@ -60,6 +60,11 @@ var validMessages = []struct { req: &Message{Token: "test-token"}, want: map[string]interface{}{"token": "test-token"}, }, + { + name: "FidOnly", + req: &Message{Fid: "test-fid"}, + want: map[string]interface{}{"fid": "test-fid"}, + }, { name: "TopicOnly", req: &Message{Topic: "test-topic"}, @@ -774,7 +779,7 @@ var invalidMessages = []struct { { name: "NoTargets", req: &Message{}, - want: "exactly one of token, topic or condition must be specified", + want: "exactly one of fid, token, topic or condition must be specified", }, { name: "MultipleTargets", @@ -782,7 +787,15 @@ var invalidMessages = []struct { Token: "token", Topic: "topic", }, - want: "exactly one of token, topic or condition must be specified", + want: "exactly one of fid, token, topic or condition must be specified", + }, + { + name: "MultipleTargetsWithFid", + req: &Message{ + Fid: "fid", + Topic: "topic", + }, + want: "exactly one of fid, token, topic or condition must be specified", }, { name: "InvalidPrefixedTopicName", diff --git a/messaging/messaging_utils.go b/messaging/messaging_utils.go index 5716b328..49fec654 100644 --- a/messaging/messaging_utils.go +++ b/messaging/messaging_utils.go @@ -33,9 +33,9 @@ func validateMessage(message *Message) error { return fmt.Errorf("message must not be nil") } - targets := countNonEmpty(message.Token, message.Condition, message.Topic) + targets := countNonEmpty(message.Token, message.Condition, message.Topic, message.Fid) if targets != 1 { - return fmt.Errorf("exactly one of token, topic or condition must be specified") + return fmt.Errorf("exactly one of fid, token, topic or condition must be specified") } // validate topic