From 3fb1538d72e7fa2b201648e8232e9a5e0fd2b10b Mon Sep 17 00:00:00 2001 From: Doris-Ge Date: Fri, 21 Apr 2023 15:36:24 -0700 Subject: [PATCH 1/4] feat(fcm): SendEach and SendEachForMulticast for FCM batch send (#552) * Implement `SendEach`, `SendEachDryRun`, `SendEachForMulticast`, (#544) `SendEachForMulticastDryRun` 1. Add `SendEach`, `SendEachDryRun`, `SendEachForMulticast`, `SendEachForMulticastDryRun` 2. Deprecate `SendAll`, `SendAllDryRun`, `SendMulticast`, `SendMulticastDryRun` `SendEach` vs `SendAll` 1. `SendEach` sends one HTTP request to V1 Send endpoint for each message in the array. `SendAll` sends only one HTTP request to V1 Batch Send endpoint to send all messages in the array. 2. `SendEach` calls fcmClient.Send to send each message and constructs a SendResponse with the returned message id or error. `SendEach` uses sync.WaitGroup to execute all fcmClient.Send calls asynchronously and wait for all of them to complete and construct a BatchResponse with all SendResponses. Therefore, unlike `SendAll`, `SendEach` does not always returns an error for a total failure. It can also return a `BatchResponse` with only errors in it. `SendEachForMulticast` calls `SendEach` under the hood. * Add integration tests for `SendEach` and `SendEachForMulticast` (#550) * Avoid using "-- i.e." in the function comments * Remove all backticks in messaging_batch.go --- integration/messaging/messaging_test.go | 124 +++++++ messaging/messaging_batch.go | 157 +++++++- messaging/messaging_batch_test.go | 456 ++++++++++++++++++++++++ 3 files changed, 724 insertions(+), 13 deletions(-) diff --git a/integration/messaging/messaging_test.go b/integration/messaging/messaging_test.go index b86aa2ae..85b475ae 100644 --- a/integration/messaging/messaging_test.go +++ b/integration/messaging/messaging_test.go @@ -105,6 +105,130 @@ func TestSendInvalidToken(t *testing.T) { } } +func TestSendEach(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.SendEachDryRun(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 TestSendEachFiveHundred(t *testing.T) { + var messages []*messaging.Message + const limit = 500 + for i := 0; i < limit; i++ { + m := &messaging.Message{ + Topic: fmt.Sprintf("foo-bar-%d", i%10), + } + messages = append(messages, m) + } + + br, err := client.SendEachDryRun(context.Background(), messages) + if err != nil { + t.Fatal(err) + } + + if len(br.Responses) != limit { + t.Errorf("len(Responses) = %d; want = %d", len(br.Responses), limit) + } + if br.SuccessCount != limit { + t.Errorf("SuccessCount = %d; want = %d", br.SuccessCount, limit) + } + if br.FailureCount != 0 { + t.Errorf("FailureCount = %d; want = 0", br.FailureCount) + } + + for i := 0; i < limit; i++ { + sr := br.Responses[i] + if err := checkSuccessfulSendResponse(sr); err != nil { + t.Errorf("Responses[%d]: %v", i, err) + } + } +} + +func TestSendEachForMulticast(t *testing.T) { + message := &messaging.MulticastMessage{ + Notification: &messaging.Notification{ + Title: "title", + Body: "body", + }, + Tokens: []string{"INVALID_TOKEN", "ANOTHER_INVALID_TOKEN"}, + } + + 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 err := checkErrorSendResponse(sr); err != nil { + t.Errorf("Responses[%d]: %v", i, err) + } + } +} + func TestSendAll(t *testing.T) { messages := []*messaging.Message{ { diff --git a/messaging/messaging_batch.go b/messaging/messaging_batch.go index b291dde1..536227d8 100644 --- a/messaging/messaging_batch.go +++ b/messaging/messaging_batch.go @@ -27,6 +27,7 @@ import ( "mime/multipart" "net/http" "net/textproto" + "sync" "firebase.google.com/go/v4/internal" ) @@ -80,21 +81,145 @@ type SendResponse struct { Error error } -// BatchResponse represents the response from the `SendAll()` and `SendMulticast()` APIs. +// BatchResponse represents the response from the SendAll() and SendMulticast() APIs. type BatchResponse struct { SuccessCount int FailureCount int Responses []*SendResponse } +// SendEach sends the messages in the given array via Firebase Cloud Messaging. +// +// The messages array may contain up to 500 messages. Unlike SendAll(), SendEach sends the entire +// array of messages by making a single HTTP call for each message. The responses list +// obtained from the return value corresponds to the order of the input messages. An error +// from SendEach 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) SendEach(ctx context.Context, messages []*Message) (*BatchResponse, error) { + return c.sendEachInBatch(ctx, messages, false) +} + +// SendEachDryRun sends the messages in the given array via Firebase Cloud Messaging 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 messages array may contain up to 500 messages. Unlike SendAllDryRun(), SendEachDryRun sends +// the entire array of messages by making a single HTTP call for each message. The responses list +// obtained from the return value corresponds to the order of the input messages. An error +// from SendEachDryRun 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) SendEachDryRun(ctx context.Context, messages []*Message) (*BatchResponse, error) { + return c.sendEachInBatch(ctx, messages, true) +} + +// SendMulticast sends the given multicast message to all the FCM registration tokens specified. +// +// 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 +// 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) { + messages, err := toMessages(message) + if err != nil { + return nil, err + } + + return c.SendEach(ctx, messages) +} + +// SendEachForMulticastDryRun sends the given multicast message to all the specified FCM registration +// tokens 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 +// indicated by a BatchResponse return value. +func (c *fcmClient) SendEachForMulticastDryRun(ctx context.Context, message *MulticastMessage) (*BatchResponse, error) { + messages, err := toMessages(message) + if err != nil { + return nil, err + } + + return c.SendEachDryRun(ctx, messages) +} + +func (c *fcmClient) sendEachInBatch(ctx context.Context, messages []*Message, dryRun bool) (*BatchResponse, error) { + if len(messages) == 0 { + return nil, errors.New("messages must not be nil or empty") + } + + if len(messages) > maxMessages { + return nil, fmt.Errorf("messages must not contain more than %d elements", maxMessages) + } + + var responses []*SendResponse = make([]*SendResponse, len(messages)) + var wg sync.WaitGroup + + for idx, m := range messages { + if err := validateMessage(m); err != nil { + return nil, fmt.Errorf("invalid message at index %d: %v", idx, err) + } + wg.Add(1) + go func(idx int, m *Message, dryRun bool, responses []*SendResponse) { + defer wg.Done() + var resp string + var err error + if dryRun { + resp, err = c.SendDryRun(ctx, m) + } else { + resp, err = c.Send(ctx, m) + } + if err == nil { + responses[idx] = &SendResponse{ + Success: true, + MessageID: resp, + } + } else { + responses[idx] = &SendResponse{ + Success: false, + Error: err, + } + } + }(idx, m, dryRun, responses) + } + // Wait for all SendDryRun/Send calls to finish + wg.Wait() + + successCount := 0 + for _, r := range responses { + if r.Success { + successCount++ + } + } + + return &BatchResponse{ + Responses: responses, + SuccessCount: successCount, + FailureCount: len(responses) - successCount, + }, nil +} + // SendAll sends the messages in the given array via Firebase Cloud Messaging. // // The messages array may contain up to 500 messages. SendAll employs batching to send the entire -// array of mssages as a single RPC call. Compared to the `Send()` function, +// array of messages as a single RPC call. Compared to the Send() function, // this is a significantly more efficient way to send multiple messages. The responses list // obtained from the return value corresponds to the order of the input messages. An error from -// SendAll indicates a total failure -- i.e. none of the messages in the array could be sent. -// Partial failures are indicated by a `BatchResponse` return value. +// SendAll indicates a total failure, meaning that none of the messages in the array could be +// sent. Partial failures are indicated by a BatchResponse return value. +// +// Deprecated: Use SendEach instead. func (c *fcmClient) SendAll(ctx context.Context, messages []*Message) (*BatchResponse, error) { return c.sendBatch(ctx, messages, false) } @@ -106,11 +231,13 @@ func (c *fcmClient) SendAll(ctx context.Context, messages []*Message) (*BatchRes // the SDK-level and backend validations on the messages, and emulates the send operation. // // The messages array may contain up to 500 messages. SendAllDryRun employs batching to send the -// entire array of mssages as a single RPC call. Compared to the `SendDryRun()` function, this +// entire array of messages as a single RPC call. Compared to the SendDryRun() function, this // is a significantly more efficient way to validate sending multiple messages. The responses list // obtained from the return value corresponds to the order of the input messages. An error from -// SendAllDryRun indicates a total failure -- i.e. none of the messages in the array could be sent -// for validation. Partial failures are indicated by a `BatchResponse` return value. +// SendAllDryRun indicates a total failure, meaning that none of the messages in the array could +// be sent for validation. Partial failures are indicated by a BatchResponse return value. +// +// Deprecated: Use SendEachDryRun instead. func (c *fcmClient) SendAllDryRun(ctx context.Context, messages []*Message) (*BatchResponse, error) { return c.sendBatch(ctx, messages, true) } @@ -118,10 +245,12 @@ func (c *fcmClient) SendAllDryRun(ctx context.Context, messages []*Message) (*Ba // SendMulticast sends the given multicast message to all the FCM registration tokens specified. // // 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 +// 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 -- i.e. the message could not be sent to any -// of the recipients. Partial failures are indicated by a `BatchResponse` return value. +// 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) { messages, err := toMessages(message) if err != nil { @@ -138,10 +267,12 @@ func (c *fcmClient) SendMulticast(ctx context.Context, message *MulticastMessage // 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 +// 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 -- i.e. none of the messages were sent to FCM for validation. Partial -// failures are indicated by a `BatchResponse` return value. +// 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) { messages, err := toMessages(message) if err != nil { diff --git a/messaging/messaging_batch_test.go b/messaging/messaging_batch_test.go index 219ce2ce..a13ca54b 100644 --- a/messaging/messaging_batch_test.go +++ b/messaging/messaging_batch_test.go @@ -26,6 +26,7 @@ import ( "net/http" "net/http/httptest" "net/textproto" + "strings" "testing" "google.golang.org/api/option" @@ -164,6 +165,412 @@ func TestMultipartEntityError(t *testing.T) { } } +func TestSendEachEmptyArray(t *testing.T) { + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + + want := "messages must not be nil or empty" + br, err := client.SendEach(ctx, nil) + if err == nil || err.Error() != want { + t.Errorf("SendEach(nil) = (%v, %v); want = (nil, %q)", br, err, want) + } + + br, err = client.SendEach(ctx, []*Message{}) + if err == nil || err.Error() != want { + t.Errorf("SendEach(nil) = (%v, %v); want = (nil, %q)", br, err, want) + } +} + +func TestSendEachTooManyMessages(t *testing.T) { + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + + var messages []*Message + for i := 0; i < 501; i++ { + messages = append(messages, &Message{Topic: "test-topic"}) + } + + want := "messages must not contain more than 500 elements" + br, err := client.SendEach(ctx, messages) + if err == nil || err.Error() != want { + t.Errorf("SendEach() = (%v, %v); want = (nil, %q)", br, err, want) + } +} + +func TestSendEachInvalidMessage(t *testing.T) { + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + + want := "invalid message at index 0: message must not be nil" + br, err := client.SendEach(ctx, []*Message{nil}) + if err == nil || err.Error() != want { + t.Errorf("SendEach() = (%v, %v); want = (nil, %q)", br, err, want) + } +} + +func TestSendEach(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, testMessage := range testMessages { + if strings.Contains(string(req), testMessage.Topic) { + 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.SendEach(ctx, testMessages) + if err != nil { + t.Fatal(err) + } + + if err := checkSuccessfulBatchResponseForSendEach(br, false); err != nil { + t.Errorf("SendEach() = %v", err) + } +} + +func TestSendEachDryRun(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, testMessage := range testMessages { + if strings.Contains(string(req), testMessage.Topic) { + 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.SendEachDryRun(ctx, testMessages) + if err != nil { + t.Fatal(err) + } + + if err := checkSuccessfulBatchResponseForSendEach(br, true); err != nil { + t.Errorf("SendEach() = %v", err) + } +} + +func TestSendEachPartialFailure(t *testing.T) { + success := []fcmResponse{ + { + Name: "projects/test-project/messages/1", + }, + } + + var failures []string + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + req, _ := ioutil.ReadAll(r.Body) + + for idx, testMessage := range testMessages { + // Write success for topic1 and error for topic2 + if strings.Contains(string(req), testMessage.Topic) { + if idx%2 == 0 { + w.Header().Set("Content-Type", wantMime) + w.Write([]byte("{ \"name\":\"" + success[0].Name + "\" }")) + } else { + w.WriteHeader(http.StatusInternalServerError) + w.Header().Set("Content-Type", wantMime) + w.Write([]byte(failures[0])) + } + } + } + })) + defer ts.Close() + + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + client.fcmEndpoint = ts.URL + + for idx, tc := range httpErrors { + failures = []string{tc.resp} + + br, err := client.SendEach(ctx, testMessages) + if err != nil { + t.Fatal(err) + } + + if err := checkPartialErrorBatchResponse(br, tc); err != nil { + t.Errorf("[%d] SendEach() = %v", idx, err) + } + } +} + +func TestSendEachTotalFailure(t *testing.T) { + var resp string + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(resp)) + })) + defer ts.Close() + + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + client.fcmEndpoint = ts.URL + client.fcmClient.httpClient.RetryConfig = nil + + for idx, tc := range httpErrors { + resp = tc.resp + br, err := client.SendEach(ctx, testMessages) + if err != nil { + t.Fatal(err) + } + + if err := checkTotalErrorBatchResponse(br, tc); err != nil { + t.Errorf("[%d] SendEach() = %v", idx, err) + } + } +} + +func TestSendEachForMulticastNil(t *testing.T) { + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + + want := "message must not be nil" + br, err := client.SendEachForMulticast(ctx, nil) + if err == nil || err.Error() != want { + t.Errorf("SendEachForMulticast(nil) = (%v, %v); want = (nil, %q)", br, err, want) + } + + br, err = client.SendEachForMulticastDryRun(ctx, nil) + if err == nil || err.Error() != want { + t.Errorf("SendEachForMulticast(nil) = (%v, %v); want = (nil, %q)", br, err, want) + } +} + +func TestSendEachForMulticastEmptyArray(t *testing.T) { + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + + want := "tokens must not be nil or empty" + mm := &MulticastMessage{} + br, err := client.SendEachForMulticast(ctx, mm) + if err == nil || err.Error() != want { + t.Errorf("SendEachForMulticast(Tokens: nil) = (%v, %v); want = (nil, %q)", br, err, want) + } + + var tokens []string + mm = &MulticastMessage{ + Tokens: tokens, + } + br, err = client.SendEachForMulticast(ctx, mm) + if err == nil || err.Error() != want { + t.Errorf("SendEachForMulticast(Tokens: []) = (%v, %v); want = (nil, %q)", br, err, want) + } +} + +func TestSendEachForMulticastTooManyTokens(t *testing.T) { + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + + var tokens []string + for i := 0; i < 501; i++ { + tokens = append(tokens, fmt.Sprintf("token%d", i)) + } + + want := "tokens must not contain more than 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) + } +} + +func TestSendEachForMulticastInvalidMessage(t *testing.T) { + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + + want := "invalid message at index 0: priority must be 'normal' or 'high'" + mm := &MulticastMessage{ + Tokens: []string{"token1"}, + Android: &AndroidConfig{ + Priority: "invalid", + }, + } + br, err := client.SendEachForMulticast(ctx, mm) + if err == nil || err.Error() != want { + t.Errorf("SendEachForMulticast() = (%v, %v); want = (nil, %q)", br, err, want) + } +} + +func TestSendEachForMulticast(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 testMulticastMessage.Tokens { + if strings.Contains(string(req), token) { + 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, testMulticastMessage) + 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) + w.Header().Set("Content-Type", "application/json") + for idx, token := range testMulticastMessage.Tokens { + if strings.Contains(string(req), token) { + w.Write([]byte("{ \"name\":\"" + testSuccessResponse[idx].Name + "\" }")) + } + } + })) + defer ts.Close() + + ctx := context.Background() + + conf := *testMessagingConfig + optEndpoint := option.WithEndpoint(ts.URL) + conf.Opts = append(conf.Opts, optEndpoint) + + client, err := NewClient(ctx, &conf) + if err != nil { + t.Fatal(err) + } + + if ts.URL != client.fcmEndpoint { + t.Errorf("client.fcmEndpoint = %q; want = %q", client.fcmEndpoint, ts.URL) + } + + br, err := client.SendEachForMulticast(ctx, testMulticastMessage) + if err := checkSuccessfulBatchResponseForSendEach(br, false); err != nil { + t.Errorf("SendEachForMulticast() = %v", err) + } +} + +func TestSendEachForMulticastDryRun(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 testMulticastMessage.Tokens { + if strings.Contains(string(req), token) { + 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.SendEachForMulticastDryRun(ctx, testMulticastMessage) + if err != nil { + t.Fatal(err) + } + + if err := checkSuccessfulBatchResponseForSendEach(br, true); err != nil { + t.Errorf("SendEachForMulticastDryRun() = %v", err) + } +} + +func TestSendEachForMulticastPartialFailure(t *testing.T) { + success := []fcmResponse{ + { + Name: "projects/test-project/messages/1", + }, + } + + var failures []string + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + req, _ := ioutil.ReadAll(r.Body) + + for idx, token := range testMulticastMessage.Tokens { + if strings.Contains(string(req), token) { + // Write success for token1 and error for token2 + if idx%2 == 0 { + w.Header().Set("Content-Type", wantMime) + w.Write([]byte("{ \"name\":\"" + success[0].Name + "\" }")) + } else { + w.WriteHeader(http.StatusInternalServerError) + w.Header().Set("Content-Type", wantMime) + w.Write([]byte(failures[0])) + } + } + } + })) + defer ts.Close() + + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + client.fcmEndpoint = ts.URL + + for idx, tc := range httpErrors { + failures = []string{tc.resp} + + br, err := client.SendEachForMulticast(ctx, testMulticastMessage) + if err != nil { + t.Fatal(err) + } + + if err := checkPartialErrorBatchResponse(br, tc); err != nil { + t.Errorf("[%d] SendEachForMulticast() = %v", idx, err) + } + } +} + func TestSendAllEmptyArray(t *testing.T) { ctx := context.Background() client, err := NewClient(ctx, testMessagingConfig) @@ -630,6 +1037,26 @@ func TestSendMulticastPartialFailure(t *testing.T) { } } +func checkSuccessfulBatchResponseForSendEach(br *BatchResponse, dryRun bool) error { + if br.SuccessCount != 2 { + return fmt.Errorf("SuccessCount = %d; want = 2", br.SuccessCount) + } + if br.FailureCount != 0 { + return fmt.Errorf("FailureCount = %d; want = 0", br.FailureCount) + } + if len(br.Responses) != 2 { + return fmt.Errorf("len(Responses) = %d; want = 2", len(br.Responses)) + } + + for idx, r := range br.Responses { + if err := checkSuccessfulSendResponse(r, testSuccessResponse[idx].Name); err != nil { + return fmt.Errorf("Responses[%d]: %v", idx, err) + } + } + + return nil +} + func checkSuccessfulBatchResponse(br *BatchResponse, req []byte, dryRun bool) error { if br.SuccessCount != 2 { return fmt.Errorf("SuccessCount = %d; want = 2", br.SuccessCount) @@ -654,6 +1081,35 @@ func checkSuccessfulBatchResponse(br *BatchResponse, req []byte, dryRun bool) er return nil } +func checkTotalErrorBatchResponse(br *BatchResponse, tc struct { + resp, want string + check func(error) bool +}) error { + if br.SuccessCount != 0 { + return fmt.Errorf("SuccessCount = %d; want = 0", br.SuccessCount) + } + if br.FailureCount != 2 { + return fmt.Errorf("FailureCount = %d; want = 2", br.FailureCount) + } + if len(br.Responses) != 2 { + return fmt.Errorf("len(Responses) = %d; want = 2", len(br.Responses)) + } + + for i, r := range br.Responses { + if r.Success { + return fmt.Errorf("Responses[%d]: Success = true; want = false", i) + } + if r.Error == nil || r.Error.Error() != tc.want || !tc.check(r.Error) { + return fmt.Errorf("Responses[%d]: Error = %v; want = %q", i, r.Error, tc.want) + } + if r.MessageID != "" { + return fmt.Errorf("Responses[%d]: MessageID = %q; want = %q", i, r.MessageID, "") + } + } + + return nil +} + func checkPartialErrorBatchResponse(br *BatchResponse, tc struct { resp, want string check func(error) bool From 04fd87fb72214c77a136bc279c7ae4b5a7ba0ab9 Mon Sep 17 00:00:00 2001 From: jibin jose Date: Wed, 12 Jul 2023 04:03:15 +1200 Subject: [PATCH 2/4] fix: typo (#563) --- auth/token_verifier.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/token_verifier.go b/auth/token_verifier.go index 0b1c54bc..3d28698a 100644 --- a/auth/token_verifier.go +++ b/auth/token_verifier.go @@ -174,7 +174,7 @@ func (tv *tokenVerifier) VerifyToken(ctx context.Context, token string, isEmulat return payload, nil } - // Verifying the signature requires syncronized access to a key cache and + // Verifying the signature requires synchronized access to a key cache and // potentially issues an http request. Therefore we do it last. if err := tv.verifySignature(ctx, token); err != nil { return nil, err From fc53aa894194c5dea6b696526c5361a257834cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ros=C3=A1rio=20P=2E=20Fernandes?= Date: Tue, 11 Jul 2023 17:33:11 +0100 Subject: [PATCH 3/4] docs: update README.md for Go v1.17 (#564) * docs: update README.md for Go v1.17 * use a generic example for specific version --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 89c6afe2..1fd81858 100644 --- a/README.md +++ b/README.md @@ -26,11 +26,14 @@ For more information, visit the ## Installation -To install Firebase Admin Go SDK, simply execute the following command -in a terminal from your `$GOPATH`: +The Firebase Admin Go SDK can be installed using the `go install` utility: ``` -go get firebase.google.com/go +# Install the latest version: +go install firebase.google.com/go/v4@latest + +# Or install a specific version: +go install firebase.google.com/go/v4@4.x.x ``` ## Contributing From 2670a8ed173edbd5c5724cff7a25dd39e7669bfb Mon Sep 17 00:00:00 2001 From: Lahiru Maramba Date: Wed, 12 Jul 2023 15:48:37 -0400 Subject: [PATCH 4/4] [chore] Release 4.12.0 (#567) --- firebase.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase.go b/firebase.go index a06829b1..fb3b0b47 100644 --- a/firebase.go +++ b/firebase.go @@ -39,7 +39,7 @@ import ( var defaultAuthOverrides = make(map[string]interface{}) // Version of the Firebase Go Admin SDK. -const Version = "4.11.0" +const Version = "4.12.0" // firebaseEnvName is the name of the environment variable with the Config. const firebaseEnvName = "FIREBASE_CONFIG"