From f6208a41b0f10f2b65e6749eabe8ece7f466a373 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 26 Dec 2017 14:18:52 +0100 Subject: [PATCH 01/17] initial commit for adding Firebase Cloud Messaging --- firebase.go | 11 +++ firebase_test.go | 12 +++ internal/internal.go | 6 ++ messaging/messaging.go | 182 ++++++++++++++++++++++++++++++++++++ messaging/messaging_test.go | 78 ++++++++++++++++ 5 files changed, 289 insertions(+) create mode 100644 messaging/messaging.go create mode 100644 messaging/messaging_test.go diff --git a/firebase.go b/firebase.go index c0ed3569..95b43bf3 100644 --- a/firebase.go +++ b/firebase.go @@ -25,6 +25,7 @@ import ( "firebase.google.com/go/auth" "firebase.google.com/go/iid" "firebase.google.com/go/internal" + "firebase.google.com/go/messaging" "firebase.google.com/go/storage" "os" @@ -42,6 +43,7 @@ var firebaseScopes = []string{ "https://www.googleapis.com/auth/firebase", "https://www.googleapis.com/auth/identitytoolkit", "https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/firebase.messaging", } // Version of the Firebase Go Admin SDK. @@ -99,6 +101,15 @@ func (a *App) InstanceID(ctx context.Context) (*iid.Client, error) { return iid.NewClient(ctx, conf) } +// Messaging returns an instance of messaging.Client. +func (a *App) Messaging(ctx context.Context) (*messaging.Client, error) { + conf := &internal.MessagingConfig{ + ProjectID: a.projectID, + Opts: a.opts, + } + return messaging.NewClient(ctx, conf) +} + // NewApp creates a new App from the provided config and client options. // // If the client options contain a valid credential (a service account file, a refresh token file or an diff --git a/firebase_test.go b/firebase_test.go index 9e2388ce..b5e8835c 100644 --- a/firebase_test.go +++ b/firebase_test.go @@ -293,6 +293,18 @@ func TestInstanceID(t *testing.T) { } } +func TestMessaging(t *testing.T) { + ctx := context.Background() + app, err := NewApp(ctx, nil, option.WithCredentialsFile("testdata/service_account.json")) + if err != nil { + t.Fatal(err) + } + + if c, err := app.Messaging(ctx); c == nil || err != nil { + t.Errorf("Messaging() = (%v, %v); want (iid, nil)", c, err) + } +} + func TestCustomTokenSource(t *testing.T) { ctx := context.Background() ts := &testTokenSource{AccessToken: "mock-token-from-custom"} diff --git a/internal/internal.go b/internal/internal.go index 34c4f32d..02e54f8c 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -46,6 +46,12 @@ type MockTokenSource struct { AccessToken string } +// MessagingConfig represents the configuration of Firebase Cloud Messaging service. +type MessagingConfig struct { + Opts []option.ClientOption + ProjectID string +} + // Token returns the test token associated with the TokenSource. func (ts *MockTokenSource) Token() (*oauth2.Token, error) { return &oauth2.Token{AccessToken: ts.AccessToken}, nil diff --git a/messaging/messaging.go b/messaging/messaging.go new file mode 100644 index 00000000..eb65f11e --- /dev/null +++ b/messaging/messaging.go @@ -0,0 +1,182 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package messaging contains functions for sending messages and managing +// device subscriptions with Firebase Cloud Messaging. +package messaging + +import ( + "context" + "errors" + "fmt" + "net/http" + + "firebase.google.com/go/internal" + "google.golang.org/api/transport" +) + +const messagingEndpoint = "https://fcm.googleapis.com/v1" + +var errorCodes = map[int]string{ + 400: "malformed argument", + 401: "request not authorized", + 403: "project does not match or the client does not have sufficient privileges", + 404: "failed to find the ...", + 409: "already deleted", + 429: "request throttled out by the backend server", + 500: "internal server error", + 503: "backend servers are over capacity", +} + +// Client is the interface for the Firebase Messaging service. +type Client struct { + // To enable testing against arbitrary endpoints. + endpoint string + client *internal.HTTPClient + project string +} + +// RequestMessage is the request body message to send by Firebase Cloud Messaging Service. +// See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send +type RequestMessage struct { + ValidateOnly bool `json:"validate_only"` + Message Message `json:"message"` +} + +// ResponseMessage is the identifier of the message sent. +// See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages +type ResponseMessage struct { + Name string `json:"name"` +} + +// Message is the message to send by Firebase Cloud Messaging Service. +// See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#Message +type Message struct { + Name string `json:"name"` + Data map[string]string `json:"data"` + Notification Notification `json:"notification,omitempty"` + Android AndroidConfig `json:"android,omitempty"` + Webpush WebpushConfig `json:"webpush,omitempty"` + Apns ApnsConfig `json:"apns,omitempty"` + Token string `json:"token,omitempty"` + Topic string `json:"topic,omitempty"` + Condition string `json:"condition,omitempty"` +} + +// Notification is the Basic notification template to use across all platforms. +// See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#Notification +type Notification struct { + Title string `json:"title,omitempty"` + Body string `json:"body,omitempty"` +} + +// AndroidConfig is Android specific options for messages. +// See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#AndroidConfig +type AndroidConfig struct { + CollapseKey string `json:"collapse_key,omitempty"` + Priority string `json:"priority,omitempty"` + TTL string `json:"ttl,omitempty"` + RestrictedPackageName string `json:"restricted_package_name,omitempty"` + Data map[string]string `json:"data,omitempty"` + Notification AndroidNotification `json:"notification,omitempty"` +} + +// AndroidNotification is notification to send to android devices. +// See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#AndroidNotification +type AndroidNotification struct { + Title string `json:"title,omitempty"` + Body string `json:"body,omitempty"` + Icon string `json:"icon,omitempty"` + Color string `json:"color,omitempty"` + Sound string `json:"sound,omitempty"` + Tag string `json:"tag,omitempty"` + ClickAction string `json:"click_action,omitempty"` + BodyLocKey string `json:"body_loc_key,omitempty"` + BodyLocArgs []string `json:"body_loc_args,omitempty"` + TitleLocKey string `json:"title_loc_key,omitempty"` + TitleLocArgs []string `json:"title_loc_args,omitempty"` +} + +// WebpushConfig is Webpush protocol options. +// See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#WebpushConfig +type WebpushConfig struct { + Headers map[string]string `json:"headers,omitempty"` + Data map[string]string `json:"data,omitempty"` + Notification WebpushNotification `json:"notification,omitempty"` +} + +// WebpushNotification is Web notification to send via webpush protocol. +// See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#WebpushNotification +type WebpushNotification struct { + Title string `json:"title,omitempty"` + Body string `json:"body,omitempty"` + Icon string `json:"icon,omitempty"` +} + +// ApnsConfig is Apple Push Notification Service specific options. +// See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#ApnsConfig +type ApnsConfig struct { + Headers map[string]string `json:"headers,omitempty"` + Payload map[string]interface{} `json:"payload,omitempty"` +} + +// NewClient creates a new instance of the Firebase Cloud Messaging Client. +// +// This function can only be invoked from within the SDK. Client applications should access the +// the Messaging service through firebase.App. +func NewClient(ctx context.Context, c *internal.MessagingConfig) (*Client, error) { + if c.ProjectID == "" { + return nil, errors.New("project id is required to access firebase cloud messaging client") + } + + hc, _, err := transport.NewHTTPClient(ctx, c.Opts...) + if err != nil { + return nil, err + } + + return &Client{ + endpoint: messagingEndpoint, + client: &internal.HTTPClient{Client: hc}, + project: c.ProjectID, + }, nil +} + +// SendMessage sends a Message to Firebase Cloud Messaging. +// +// Send a message to specified target (a registration token, topic or condition). +// https://firebase.google.com/docs/cloud-messaging/send-message +func (c *Client) SendMessage(ctx context.Context, payload RequestMessage) (msg *ResponseMessage, err error) { + result := &ResponseMessage{} + if payload.Message.Token == "" && payload.Message.Condition == "" && payload.Message.Topic == "" { + return result, fmt.Errorf("target message is empty %v", payload) + } + + request := &internal.Request{ + Method: http.MethodPost, + URL: fmt.Sprintf("%s/project/%s/messages:send", c.endpoint, c.project), + Body: internal.NewJSONEntity(payload), + } + resp, err := c.client.Do(ctx, request) + if err != nil { + return result, err + } + + if msg, ok := errorCodes[resp.Status]; ok { + return result, fmt.Errorf("project id %q: %s", c.project, msg) + } + + err = resp.Unmarshal(http.StatusOK, result) + + return result, err +} diff --git a/messaging/messaging_test.go b/messaging/messaging_test.go new file mode 100644 index 00000000..7c08c2d6 --- /dev/null +++ b/messaging/messaging_test.go @@ -0,0 +1,78 @@ +package messaging + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "google.golang.org/api/option" + + "firebase.google.com/go/internal" +) + +var testMessagingConfig = &internal.MessagingConfig{ + ProjectID: "test-project", + Opts: []option.ClientOption{ + option.WithTokenSource(&internal.MockTokenSource{AccessToken: "test-token"}), + }, +} + +func TestNoProjectID(t *testing.T) { + client, err := NewClient(context.Background(), &internal.MessagingConfig{}) + if client != nil || err == nil { + t.Errorf("NewClient() = (%v, %v); want = (nil, error)", client, err) + } +} + +func TestEmptyTarget(t *testing.T) { + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + + _, err = client.SendMessage(ctx, RequestMessage{}) + if err == nil { + t.Errorf("SendMessage(Message{empty}) = nil; want error") + } +} + +func TestSendMessage(t *testing.T) { + var tr *http.Request + msgName := "projects/test-project/messages/0:1500415314455276%31bd1c9631bd1c96" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tr = r + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{ \"Name\":\"" + msgName + "\" }")) + })) + defer ts.Close() + + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + client.endpoint = ts.URL + msg, err := client.SendMessage(ctx, RequestMessage{Message: Message{Topic: "my-topic"}}) + if err != nil { + t.Errorf("SendMessage() = %v; want nil", err) + } + + if msg.Name != msgName { + t.Errorf("response Name = %q; want = %q", msg.Name, msgName) + } + + if tr.Body == nil { + t.Fatalf("Request = nil; want non-nil") + } + if tr.Method != "POST" { + t.Errorf("Method = %q; want = %q", tr.Method, "POST") + } + if tr.URL.Path != "/project/test-project/messages:send" { + t.Errorf("Path = %q; want = %q", tr.URL.Path, "/project/test-project/messages:send") + } + if h := tr.Header.Get("Authorization"); h != "Bearer test-token" { + t.Errorf("Authorization = %q; want = %q", h, "Bearer test-token") + } +} From 07591068bc16eba221e4308afec28985c1813bb8 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 26 Dec 2017 19:42:59 +0100 Subject: [PATCH 02/17] add validator --- messaging/messaging.go | 60 ++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/messaging/messaging.go b/messaging/messaging.go index eb65f11e..c7977361 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -63,15 +63,15 @@ type ResponseMessage struct { // Message is the message to send by Firebase Cloud Messaging Service. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#Message type Message struct { - Name string `json:"name"` - Data map[string]string `json:"data"` - Notification Notification `json:"notification,omitempty"` - Android AndroidConfig `json:"android,omitempty"` - Webpush WebpushConfig `json:"webpush,omitempty"` - Apns ApnsConfig `json:"apns,omitempty"` - Token string `json:"token,omitempty"` - Topic string `json:"topic,omitempty"` - Condition string `json:"condition,omitempty"` + Name string `json:"name"` + Data map[string]interface{} `json:"data"` + Notification Notification `json:"notification,omitempty"` + Android AndroidConfig `json:"android,omitempty"` + Webpush WebpushConfig `json:"webpush,omitempty"` + Apns ApnsConfig `json:"apns,omitempty"` + Token string `json:"token,omitempty"` + Topic string `json:"topic,omitempty"` + Condition string `json:"condition,omitempty"` } // Notification is the Basic notification template to use across all platforms. @@ -84,12 +84,12 @@ type Notification struct { // AndroidConfig is Android specific options for messages. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#AndroidConfig type AndroidConfig struct { - CollapseKey string `json:"collapse_key,omitempty"` - Priority string `json:"priority,omitempty"` - TTL string `json:"ttl,omitempty"` - RestrictedPackageName string `json:"restricted_package_name,omitempty"` - Data map[string]string `json:"data,omitempty"` - Notification AndroidNotification `json:"notification,omitempty"` + CollapseKey string `json:"collapse_key,omitempty"` + Priority string `json:"priority,omitempty"` + TTL string `json:"ttl,omitempty"` + RestrictedPackageName string `json:"restricted_package_name,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` + Notification AndroidNotification `json:"notification,omitempty"` } // AndroidNotification is notification to send to android devices. @@ -111,9 +111,9 @@ type AndroidNotification struct { // WebpushConfig is Webpush protocol options. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#WebpushConfig type WebpushConfig struct { - Headers map[string]string `json:"headers,omitempty"` - Data map[string]string `json:"data,omitempty"` - Notification WebpushNotification `json:"notification,omitempty"` + Headers map[string]interface{} `json:"headers,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` + Notification WebpushNotification `json:"notification,omitempty"` } // WebpushNotification is Web notification to send via webpush protocol. @@ -157,9 +157,8 @@ func NewClient(ctx context.Context, c *internal.MessagingConfig) (*Client, error // Send a message to specified target (a registration token, topic or condition). // https://firebase.google.com/docs/cloud-messaging/send-message func (c *Client) SendMessage(ctx context.Context, payload RequestMessage) (msg *ResponseMessage, err error) { - result := &ResponseMessage{} - if payload.Message.Token == "" && payload.Message.Condition == "" && payload.Message.Topic == "" { - return result, fmt.Errorf("target message is empty %v", payload) + if err := validateTarget(payload); err != nil { + return nil, err } request := &internal.Request{ @@ -169,14 +168,29 @@ func (c *Client) SendMessage(ctx context.Context, payload RequestMessage) (msg * } resp, err := c.client.Do(ctx, request) if err != nil { - return result, err + return nil, err } if msg, ok := errorCodes[resp.Status]; ok { - return result, fmt.Errorf("project id %q: %s", c.project, msg) + return nil, fmt.Errorf("project id %q: %s", c.project, msg) } + result := &ResponseMessage{} err = resp.Unmarshal(http.StatusOK, result) return result, err } + +// validators + +// TODO add validator : Data messages can have a 4KB maximum payload. +// TODO add validator : topic name reg expression: "[a-zA-Z0-9-_.~%]+". +// TODO add validator : Conditions for topics support two operators per +// expression, and parentheses are supported. + +func validateTarget(payload RequestMessage) error { + if payload.Message.Token == "" && payload.Message.Condition == "" && payload.Message.Topic == "" { + return fmt.Errorf("target is empty you have to fill one of this fields (Token, Condition, Topic)") + } + return nil +} From a0ba55c0b058eed0c5af249e3212eeb0aee79acf Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 26 Dec 2017 22:29:12 +0100 Subject: [PATCH 03/17] use http const in messaging test --- messaging/messaging_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messaging/messaging_test.go b/messaging/messaging_test.go index 7c08c2d6..e559f69f 100644 --- a/messaging/messaging_test.go +++ b/messaging/messaging_test.go @@ -66,8 +66,8 @@ func TestSendMessage(t *testing.T) { if tr.Body == nil { t.Fatalf("Request = nil; want non-nil") } - if tr.Method != "POST" { - t.Errorf("Method = %q; want = %q", tr.Method, "POST") + if tr.Method != http.MethodPost { + t.Errorf("Method = %q; want = %q", tr.Method, http.MethodPost) } if tr.URL.Path != "/project/test-project/messages:send" { t.Errorf("Path = %q; want = %q", tr.URL.Path, "/project/test-project/messages:send") From c40516b05a2e7f2fa1811f173c15fbe1ff73104c Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 26 Dec 2017 22:52:11 +0100 Subject: [PATCH 04/17] add client version header for stats --- firebase.go | 1 + internal/internal.go | 1 + messaging/messaging.go | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/firebase.go b/firebase.go index 95b43bf3..a14ecb92 100644 --- a/firebase.go +++ b/firebase.go @@ -106,6 +106,7 @@ func (a *App) Messaging(ctx context.Context) (*messaging.Client, error) { conf := &internal.MessagingConfig{ ProjectID: a.projectID, Opts: a.opts, + Version: Version, } return messaging.NewClient(ctx, conf) } diff --git a/internal/internal.go b/internal/internal.go index 02e54f8c..225edc9e 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -50,6 +50,7 @@ type MockTokenSource struct { type MessagingConfig struct { Opts []option.ClientOption ProjectID string + Version string } // Token returns the test token associated with the TokenSource. diff --git a/messaging/messaging.go b/messaging/messaging.go index c7977361..96916ed6 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -45,6 +45,7 @@ type Client struct { endpoint string client *internal.HTTPClient project string + version string } // RequestMessage is the request body message to send by Firebase Cloud Messaging Service. @@ -149,6 +150,7 @@ func NewClient(ctx context.Context, c *internal.MessagingConfig) (*Client, error endpoint: messagingEndpoint, client: &internal.HTTPClient{Client: hc}, project: c.ProjectID, + version: "Go/Admin/" + c.Version, }, nil } @@ -161,10 +163,12 @@ func (c *Client) SendMessage(ctx context.Context, payload RequestMessage) (msg * return nil, err } + versionHeader := internal.WithHeader("X-Client-Version", c.version) request := &internal.Request{ Method: http.MethodPost, URL: fmt.Sprintf("%s/project/%s/messages:send", c.endpoint, c.project), Body: internal.NewJSONEntity(payload), + Opts: []internal.HTTPOption{versionHeader}, } resp, err := c.client.Do(ctx, request) if err != nil { From c91fb40fe75433735c52b7c2c35f8217b501e985 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Fri, 29 Dec 2017 20:56:17 +0100 Subject: [PATCH 05/17] init integration test --- integration/messaging/messaging_test.go | 73 +++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 integration/messaging/messaging_test.go diff --git a/integration/messaging/messaging_test.go b/integration/messaging/messaging_test.go new file mode 100644 index 00000000..b1ccfaf8 --- /dev/null +++ b/integration/messaging/messaging_test.go @@ -0,0 +1,73 @@ +package messaging + +import ( + "context" + "flag" + "log" + "os" + "testing" + + "firebase.google.com/go/integration/internal" + "firebase.google.com/go/messaging" +) + +var client *messaging.Client + +func TestMain(m *testing.M) { + flag.Parse() + if testing.Short() { + log.Println("skipping Messaging integration tests in short mode.") + return + } + + ctx := context.Background() + app, err := internal.NewTestApp(ctx) + if err != nil { + log.Fatalln(err) + } + + client, err = app.Messaging(ctx) + if err != nil { + log.Fatalln(err) + } + os.Exit(m.Run()) +} + +func TestSendMessageToToken(t *testing.T) { +} + +func TestSendMessageToTopic(t *testing.T) { +} + +func TestSendMessageToCondition(t *testing.T) { +} + +func TestSendNotificationMessage(t *testing.T) { +} + +func TestSendDataMessage(t *testing.T) { +} + +func TestSendAndroidNotificationMessage(t *testing.T) { +} + +func TestSendAndroidDataMessage(t *testing.T) { +} + +func TestSendApnsNotificationMessage(t *testing.T) { +} + +func TestSendApnsDataMessage(t *testing.T) { +} + +func TestSendWebPushNotificationMessage(t *testing.T) { +} + +func TestSendWebPushDataMessage(t *testing.T) { +} + +func TestSendMultiotificationMessage(t *testing.T) { +} + +func TestSendMultiDataMessage(t *testing.T) { +} From d312b7c8b13b0c331af97d6cd176c6ed36014f85 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 2 Jan 2018 17:15:25 +0100 Subject: [PATCH 06/17] add integration test (validated on IOS today) --- integration/messaging/messaging_test.go | 68 +++++++++++++++++++++++++ messaging/messaging.go | 19 +++---- 2 files changed, 78 insertions(+), 9 deletions(-) diff --git a/integration/messaging/messaging_test.go b/integration/messaging/messaging_test.go index b1ccfaf8..f7fc5c2f 100644 --- a/integration/messaging/messaging_test.go +++ b/integration/messaging/messaging_test.go @@ -3,6 +3,7 @@ package messaging import ( "context" "flag" + "fmt" "log" "os" "testing" @@ -11,6 +12,7 @@ import ( "firebase.google.com/go/messaging" ) +var projectID string var client *messaging.Client func TestMain(m *testing.M) { @@ -26,14 +28,80 @@ func TestMain(m *testing.M) { log.Fatalln(err) } + projectID, err = internal.ProjectID() + if err != nil { + log.Fatalln(err) + } + client, err = app.Messaging(ctx) + if err != nil { log.Fatalln(err) } os.Exit(m.Run()) } +func TestSendMessageInvalidToken(t *testing.T) { + ctx := context.Background() + msg := &messaging.RequestMessage{ + Message: messaging.Message{ + Token: "INVALID_TOKEN", + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + }, + } + _, err := client.SendMessage(ctx, msg) + + if err == nil { + log.Fatal(err) + } +} + +func TestSendMessageValidateOnly(t *testing.T) { + ctx := context.Background() + msg := &messaging.RequestMessage{ + ValidateOnly: true, + Message: messaging.Message{ + Token: "TODO integration_messaging.json", + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + }, + } + resp, err := client.SendMessage(ctx, msg) + + if err != nil { + log.Fatal(resp) + } + + if resp.Name != fmt.Sprintf("projects/%s/messages/fake_message_id", projectID) { + t.Errorf("Name : %s; want : projects/%s/messages/fake_message_id", resp.Name, projectID) + } +} + func TestSendMessageToToken(t *testing.T) { + ctx := context.Background() + msg := &messaging.RequestMessage{ + Message: messaging.Message{ + Token: "TODO integration_messaging.json", + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + }, + } + resp, err := client.SendMessage(ctx, msg) + + if err != nil { + log.Fatal(err) + } + + if resp.Name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + } } func TestSendMessageToTopic(t *testing.T) { diff --git a/messaging/messaging.go b/messaging/messaging.go index 96916ed6..73839d45 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -51,8 +51,8 @@ type Client struct { // RequestMessage is the request body message to send by Firebase Cloud Messaging Service. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send type RequestMessage struct { - ValidateOnly bool `json:"validate_only"` - Message Message `json:"message"` + ValidateOnly bool `json:"validate_only,omitempty"` + Message Message `json:"message,omitempty"` } // ResponseMessage is the identifier of the message sent. @@ -64,8 +64,8 @@ type ResponseMessage struct { // Message is the message to send by Firebase Cloud Messaging Service. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#Message type Message struct { - Name string `json:"name"` - Data map[string]interface{} `json:"data"` + Name string `json:"name,omitempty"` + Data map[string]interface{} `json:"data,omitempty"` Notification Notification `json:"notification,omitempty"` Android AndroidConfig `json:"android,omitempty"` Webpush WebpushConfig `json:"webpush,omitempty"` @@ -158,15 +158,16 @@ func NewClient(ctx context.Context, c *internal.MessagingConfig) (*Client, error // // Send a message to specified target (a registration token, topic or condition). // https://firebase.google.com/docs/cloud-messaging/send-message -func (c *Client) SendMessage(ctx context.Context, payload RequestMessage) (msg *ResponseMessage, err error) { +func (c *Client) SendMessage(ctx context.Context, payload *RequestMessage) (msg *ResponseMessage, err error) { if err := validateTarget(payload); err != nil { return nil, err } versionHeader := internal.WithHeader("X-Client-Version", c.version) + request := &internal.Request{ Method: http.MethodPost, - URL: fmt.Sprintf("%s/project/%s/messages:send", c.endpoint, c.project), + URL: fmt.Sprintf("%s/projects/%s/messages:send", c.endpoint, c.project), Body: internal.NewJSONEntity(payload), Opts: []internal.HTTPOption{versionHeader}, } @@ -175,8 +176,8 @@ func (c *Client) SendMessage(ctx context.Context, payload RequestMessage) (msg * return nil, err } - if msg, ok := errorCodes[resp.Status]; ok { - return nil, fmt.Errorf("project id %q: %s", c.project, msg) + if _, ok := errorCodes[resp.Status]; ok { + return nil, fmt.Errorf("unexpected http status code : %d, reason: %v", resp.Status, string(resp.Body)) } result := &ResponseMessage{} @@ -192,7 +193,7 @@ func (c *Client) SendMessage(ctx context.Context, payload RequestMessage) (msg * // TODO add validator : Conditions for topics support two operators per // expression, and parentheses are supported. -func validateTarget(payload RequestMessage) error { +func validateTarget(payload *RequestMessage) error { if payload.Message.Token == "" && payload.Message.Condition == "" && payload.Message.Topic == "" { return fmt.Errorf("target is empty you have to fill one of this fields (Token, Condition, Topic)") } From 0e83458a2741d279352c1c0e40b9c81f714fd581 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 2 Jan 2018 17:18:29 +0100 Subject: [PATCH 07/17] add comment with URL to enable Firebase Cloud Messaging API --- integration/messaging/messaging_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration/messaging/messaging_test.go b/integration/messaging/messaging_test.go index f7fc5c2f..5a6dd09f 100644 --- a/integration/messaging/messaging_test.go +++ b/integration/messaging/messaging_test.go @@ -15,6 +15,8 @@ import ( var projectID string var client *messaging.Client +// Enable API before testing +// https://console.developers.google.com/apis/library/fcm.googleapis.com/?project= func TestMain(m *testing.M) { flag.Parse() if testing.Short() { From 94cd35e66858dc9aaeb19ad52a4263512cf5313e Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 2 Jan 2018 19:02:14 +0100 Subject: [PATCH 08/17] fix broken test --- messaging/messaging_test.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/messaging/messaging_test.go b/messaging/messaging_test.go index e559f69f..a94762e7 100644 --- a/messaging/messaging_test.go +++ b/messaging/messaging_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/http/httptest" + "strings" "testing" "google.golang.org/api/option" @@ -32,7 +33,7 @@ func TestEmptyTarget(t *testing.T) { t.Fatal(err) } - _, err = client.SendMessage(ctx, RequestMessage{}) + _, err = client.SendMessage(ctx, &RequestMessage{}) if err == nil { t.Errorf("SendMessage(Message{empty}) = nil; want error") } @@ -54,7 +55,7 @@ func TestSendMessage(t *testing.T) { t.Fatal(err) } client.endpoint = ts.URL - msg, err := client.SendMessage(ctx, RequestMessage{Message: Message{Topic: "my-topic"}}) + msg, err := client.SendMessage(ctx, &RequestMessage{Message: Message{Topic: "my-topic"}}) if err != nil { t.Errorf("SendMessage() = %v; want nil", err) } @@ -63,14 +64,18 @@ func TestSendMessage(t *testing.T) { t.Errorf("response Name = %q; want = %q", msg.Name, msgName) } + if !strings.HasPrefix(msg.Name, "projects/test-project/messages/") { + t.Errorf("response Name = %q; want prefix = %q", msg.Name, "projects/test-project/messages/") + } + if tr.Body == nil { t.Fatalf("Request = nil; want non-nil") } if tr.Method != http.MethodPost { t.Errorf("Method = %q; want = %q", tr.Method, http.MethodPost) } - if tr.URL.Path != "/project/test-project/messages:send" { - t.Errorf("Path = %q; want = %q", tr.URL.Path, "/project/test-project/messages:send") + if tr.URL.Path != "/projects/test-project/messages:send" { + t.Errorf("Path = %q; want = %q", tr.URL.Path, "/projects/test-project/messages:send") } if h := tr.Header.Get("Authorization"); h != "Bearer test-token" { t.Errorf("Authorization = %q; want = %q", h, "Bearer test-token") From 6d761057b1f855fa6fec7f6c6cffcdbb541e1846 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 2 Jan 2018 19:52:57 +0100 Subject: [PATCH 09/17] add integration tests --- integration/messaging/messaging_test.go | 219 ++++++++++++++++++++++-- messaging/messaging.go | 2 +- 2 files changed, 208 insertions(+), 13 deletions(-) diff --git a/integration/messaging/messaging_test.go b/integration/messaging/messaging_test.go index 5a6dd09f..40e88df1 100644 --- a/integration/messaging/messaging_test.go +++ b/integration/messaging/messaging_test.go @@ -4,6 +4,7 @@ import ( "context" "flag" "fmt" + "io/ioutil" "log" "os" "testing" @@ -15,6 +16,12 @@ import ( var projectID string var client *messaging.Client +var testFixtures = struct { + token string + topic string + condition string +}{} + // Enable API before testing // https://console.developers.google.com/apis/library/fcm.googleapis.com/?project= func TestMain(m *testing.M) { @@ -24,6 +31,24 @@ func TestMain(m *testing.M) { return } + token, err := ioutil.ReadFile(internal.Resource("integration_token.txt")) + if err != nil { + log.Fatalln(err) + } + testFixtures.token = string(token) + + topic, err := ioutil.ReadFile(internal.Resource("integration_topic.txt")) + if err != nil { + log.Fatalln(err) + } + testFixtures.topic = string(topic) + + condition, err := ioutil.ReadFile(internal.Resource("integration_condition.txt")) + if err != nil { + log.Fatalln(err) + } + testFixtures.condition = string(condition) + ctx := context.Background() app, err := internal.NewTestApp(ctx) if err != nil { @@ -66,7 +91,7 @@ func TestSendMessageValidateOnly(t *testing.T) { msg := &messaging.RequestMessage{ ValidateOnly: true, Message: messaging.Message{ - Token: "TODO integration_messaging.json", + Token: testFixtures.token, Notification: messaging.Notification{ Title: "My Title", Body: "This is a Notification", @@ -76,7 +101,7 @@ func TestSendMessageValidateOnly(t *testing.T) { resp, err := client.SendMessage(ctx, msg) if err != nil { - log.Fatal(resp) + log.Fatal(err) } if resp.Name != fmt.Sprintf("projects/%s/messages/fake_message_id", projectID) { @@ -88,7 +113,7 @@ func TestSendMessageToToken(t *testing.T) { ctx := context.Background() msg := &messaging.RequestMessage{ Message: messaging.Message{ - Token: "TODO integration_messaging.json", + Token: testFixtures.token, Notification: messaging.Notification{ Title: "My Title", Body: "This is a Notification", @@ -107,37 +132,207 @@ func TestSendMessageToToken(t *testing.T) { } func TestSendMessageToTopic(t *testing.T) { + ctx := context.Background() + msg := &messaging.RequestMessage{ + Message: messaging.Message{ + Topic: testFixtures.topic, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + }, + } + resp, err := client.SendMessage(ctx, msg) + + if err != nil { + log.Fatal(err) + } + + if resp.Name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + } } func TestSendMessageToCondition(t *testing.T) { + ctx := context.Background() + msg := &messaging.RequestMessage{ + Message: messaging.Message{ + Condition: testFixtures.condition, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + }, + } + resp, err := client.SendMessage(ctx, msg) + + if err != nil { + log.Fatal(err) + } + + if resp.Name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + } } func TestSendNotificationMessage(t *testing.T) { + ctx := context.Background() + msg := &messaging.RequestMessage{ + Message: messaging.Message{ + Token: testFixtures.token, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + }, + } + resp, err := client.SendMessage(ctx, msg) + + if err != nil { + log.Fatal(err) + } + + if resp.Name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + } } func TestSendDataMessage(t *testing.T) { + ctx := context.Background() + msg := &messaging.RequestMessage{ + Message: messaging.Message{ + Token: testFixtures.token, + Data: map[string]interface{}{ + "private_key": "foo", + "client_email": "bar@test.com", + }, + }, + } + resp, err := client.SendMessage(ctx, msg) + + if err != nil { + log.Fatal(err) + } + + if resp.Name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + } } func TestSendAndroidNotificationMessage(t *testing.T) { + ctx := context.Background() + msg := &messaging.RequestMessage{ + Message: messaging.Message{ + Token: testFixtures.token, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + Android: messaging.AndroidConfig{ + CollapseKey: "Collapse", + Priority: "HIGH", + TTL: "3.5s", + Notification: messaging.AndroidNotification{ + Title: "Android Title", + Body: "Android body", + }, + }, + }, + } + resp, err := client.SendMessage(ctx, msg) + + if err != nil { + log.Fatal(err) + } + + if resp.Name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + } } func TestSendAndroidDataMessage(t *testing.T) { + ctx := context.Background() + msg := &messaging.RequestMessage{ + Message: messaging.Message{ + Token: testFixtures.token, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + Android: messaging.AndroidConfig{ + CollapseKey: "Collapse", + Priority: "HIGH", + TTL: "3.5s", + Data: map[string]interface{}{ + "private_key": "foo", + "client_email": "bar@test.com", + }, + }, + }, + } + resp, err := client.SendMessage(ctx, msg) + + if err != nil { + log.Fatal(err) + } + + if resp.Name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + } } func TestSendApnsNotificationMessage(t *testing.T) { -} + ctx := context.Background() + msg := &messaging.RequestMessage{ + Message: messaging.Message{ + Token: testFixtures.token, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + Apns: messaging.ApnsConfig{ + Payload: map[string]interface{}{ + "title": "APNS Title ", + "body": "APNS bodym", + }, + }, + }, + } + resp, err := client.SendMessage(ctx, msg) -func TestSendApnsDataMessage(t *testing.T) { -} + if err != nil { + log.Fatal(err) + } -func TestSendWebPushNotificationMessage(t *testing.T) { + if resp.Name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + } } -func TestSendWebPushDataMessage(t *testing.T) { -} +func TestSendApnsDataMessage(t *testing.T) { + ctx := context.Background() + msg := &messaging.RequestMessage{ + Message: messaging.Message{ + Token: testFixtures.token, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + Apns: messaging.ApnsConfig{ + Headers: map[string]interface{}{ + "private_key": "foo", + "client_email": "bar@test.com", + }, + }, + }, + } + resp, err := client.SendMessage(ctx, msg) -func TestSendMultiotificationMessage(t *testing.T) { -} + if err != nil { + log.Fatal(err) + } -func TestSendMultiDataMessage(t *testing.T) { + if resp.Name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + } } diff --git a/messaging/messaging.go b/messaging/messaging.go index 73839d45..bd658b05 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -128,7 +128,7 @@ type WebpushNotification struct { // ApnsConfig is Apple Push Notification Service specific options. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#ApnsConfig type ApnsConfig struct { - Headers map[string]string `json:"headers,omitempty"` + Headers map[string]interface{} `json:"headers,omitempty"` Payload map[string]interface{} `json:"payload,omitempty"` } From aee449c633787d93986d8388d59203a0e1cacfb1 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 2 Jan 2018 21:35:15 +0100 Subject: [PATCH 10/17] accept a Message instead of RequestMessage + and rename method + send / sendDryRun --- integration/messaging/messaging_test.go | 269 +++++++++++------------- messaging/messaging.go | 46 +++- messaging/messaging_test.go | 57 ++++- 3 files changed, 209 insertions(+), 163 deletions(-) diff --git a/integration/messaging/messaging_test.go b/integration/messaging/messaging_test.go index 40e88df1..63f85766 100644 --- a/integration/messaging/messaging_test.go +++ b/integration/messaging/messaging_test.go @@ -68,271 +68,248 @@ func TestMain(m *testing.M) { os.Exit(m.Run()) } -func TestSendMessageInvalidToken(t *testing.T) { +func TestSendInvalidToken(t *testing.T) { ctx := context.Background() - msg := &messaging.RequestMessage{ - Message: messaging.Message{ - Token: "INVALID_TOKEN", - Notification: messaging.Notification{ - Title: "My Title", - Body: "This is a Notification", - }, + msg := &messaging.Message{ + Token: "INVALID_TOKEN", + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", }, } - _, err := client.SendMessage(ctx, msg) + _, err := client.Send(ctx, msg) if err == nil { log.Fatal(err) } } -func TestSendMessageValidateOnly(t *testing.T) { +func TestSendDryRun(t *testing.T) { ctx := context.Background() - msg := &messaging.RequestMessage{ - ValidateOnly: true, - Message: messaging.Message{ - Token: testFixtures.token, - Notification: messaging.Notification{ - Title: "My Title", - Body: "This is a Notification", - }, + msg := &messaging.Message{ + Token: testFixtures.token, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", }, } - resp, err := client.SendMessage(ctx, msg) + name, err := client.SendDryRun(ctx, msg) if err != nil { log.Fatal(err) } - if resp.Name != fmt.Sprintf("projects/%s/messages/fake_message_id", projectID) { - t.Errorf("Name : %s; want : projects/%s/messages/fake_message_id", resp.Name, projectID) + if name != fmt.Sprintf("projects/%s/messages/fake_message_id", projectID) { + t.Errorf("Name : %s; want : projects/%s/messages/fake_message_id", name, projectID) } } -func TestSendMessageToToken(t *testing.T) { +func TestSendToToken(t *testing.T) { ctx := context.Background() - msg := &messaging.RequestMessage{ - Message: messaging.Message{ - Token: testFixtures.token, - Notification: messaging.Notification{ - Title: "My Title", - Body: "This is a Notification", - }, + msg := &messaging.Message{ + Token: testFixtures.token, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", }, } - resp, err := client.SendMessage(ctx, msg) + name, err := client.Send(ctx, msg) if err != nil { log.Fatal(err) } - if resp.Name == "" { - t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + if name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", name, projectID) } } -func TestSendMessageToTopic(t *testing.T) { +func TestSendToTopic(t *testing.T) { ctx := context.Background() - msg := &messaging.RequestMessage{ - Message: messaging.Message{ - Topic: testFixtures.topic, - Notification: messaging.Notification{ - Title: "My Title", - Body: "This is a Notification", - }, + msg := &messaging.Message{ + Topic: testFixtures.topic, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", }, } - resp, err := client.SendMessage(ctx, msg) + name, err := client.Send(ctx, msg) if err != nil { log.Fatal(err) } - if resp.Name == "" { - t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + if name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", name, projectID) } } -func TestSendMessageToCondition(t *testing.T) { +func TestSendToCondition(t *testing.T) { ctx := context.Background() - msg := &messaging.RequestMessage{ - Message: messaging.Message{ - Condition: testFixtures.condition, - Notification: messaging.Notification{ - Title: "My Title", - Body: "This is a Notification", - }, + msg := &messaging.Message{ + Condition: testFixtures.condition, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", }, } - resp, err := client.SendMessage(ctx, msg) + name, err := client.Send(ctx, msg) if err != nil { log.Fatal(err) } - if resp.Name == "" { - t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + if name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", name, projectID) } } -func TestSendNotificationMessage(t *testing.T) { +func TestSendNotification(t *testing.T) { ctx := context.Background() - msg := &messaging.RequestMessage{ - Message: messaging.Message{ - Token: testFixtures.token, - Notification: messaging.Notification{ - Title: "My Title", - Body: "This is a Notification", - }, + msg := &messaging.Message{ + Token: testFixtures.token, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", }, } - resp, err := client.SendMessage(ctx, msg) + name, err := client.Send(ctx, msg) if err != nil { log.Fatal(err) } - if resp.Name == "" { - t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + if name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", name, projectID) } } -func TestSendDataMessage(t *testing.T) { +func TestSendData(t *testing.T) { ctx := context.Background() - msg := &messaging.RequestMessage{ - Message: messaging.Message{ - Token: testFixtures.token, - Data: map[string]interface{}{ - "private_key": "foo", - "client_email": "bar@test.com", - }, + msg := &messaging.Message{ + Token: testFixtures.token, + Data: map[string]interface{}{ + "private_key": "foo", + "client_email": "bar@test.com", }, } - resp, err := client.SendMessage(ctx, msg) + name, err := client.Send(ctx, msg) if err != nil { log.Fatal(err) } - if resp.Name == "" { - t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + if name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", name, projectID) } } -func TestSendAndroidNotificationMessage(t *testing.T) { +func TestSendAndroidNotification(t *testing.T) { ctx := context.Background() - msg := &messaging.RequestMessage{ - Message: messaging.Message{ - Token: testFixtures.token, - Notification: messaging.Notification{ - Title: "My Title", - Body: "This is a Notification", - }, - Android: messaging.AndroidConfig{ - CollapseKey: "Collapse", - Priority: "HIGH", - TTL: "3.5s", - Notification: messaging.AndroidNotification{ - Title: "Android Title", - Body: "Android body", - }, + msg := &messaging.Message{ + Token: testFixtures.token, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + Android: messaging.AndroidConfig{ + CollapseKey: "Collapse", + Priority: "HIGH", + TTL: "3.5s", + Notification: messaging.AndroidNotification{ + Title: "Android Title", + Body: "Android body", }, }, } - resp, err := client.SendMessage(ctx, msg) + name, err := client.Send(ctx, msg) if err != nil { log.Fatal(err) } - if resp.Name == "" { - t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + if name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", name, projectID) } } -func TestSendAndroidDataMessage(t *testing.T) { +func TestSendAndroidData(t *testing.T) { ctx := context.Background() - msg := &messaging.RequestMessage{ - Message: messaging.Message{ - Token: testFixtures.token, - Notification: messaging.Notification{ - Title: "My Title", - Body: "This is a Notification", - }, - Android: messaging.AndroidConfig{ - CollapseKey: "Collapse", - Priority: "HIGH", - TTL: "3.5s", - Data: map[string]interface{}{ - "private_key": "foo", - "client_email": "bar@test.com", - }, + msg := &messaging.Message{ + Token: testFixtures.token, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + Android: messaging.AndroidConfig{ + CollapseKey: "Collapse", + Priority: "HIGH", + TTL: "3.5s", + Data: map[string]interface{}{ + "private_key": "foo", + "client_email": "bar@test.com", }, }, } - resp, err := client.SendMessage(ctx, msg) + name, err := client.Send(ctx, msg) if err != nil { log.Fatal(err) } - if resp.Name == "" { - t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + if name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", name, projectID) } } -func TestSendApnsNotificationMessage(t *testing.T) { +func TestSendApnsNotification(t *testing.T) { ctx := context.Background() - msg := &messaging.RequestMessage{ - Message: messaging.Message{ - Token: testFixtures.token, - Notification: messaging.Notification{ - Title: "My Title", - Body: "This is a Notification", - }, - Apns: messaging.ApnsConfig{ - Payload: map[string]interface{}{ - "title": "APNS Title ", - "body": "APNS bodym", - }, + msg := &messaging.Message{ + Token: testFixtures.token, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + Apns: messaging.ApnsConfig{ + Payload: map[string]interface{}{ + "title": "APNS Title ", + "body": "APNS bodym", }, }, } - resp, err := client.SendMessage(ctx, msg) + name, err := client.Send(ctx, msg) if err != nil { log.Fatal(err) } - if resp.Name == "" { - t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + if name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", name, projectID) } } -func TestSendApnsDataMessage(t *testing.T) { +func TestSendApnsData(t *testing.T) { ctx := context.Background() - msg := &messaging.RequestMessage{ - Message: messaging.Message{ - Token: testFixtures.token, - Notification: messaging.Notification{ - Title: "My Title", - Body: "This is a Notification", - }, - Apns: messaging.ApnsConfig{ - Headers: map[string]interface{}{ - "private_key": "foo", - "client_email": "bar@test.com", - }, + msg := &messaging.Message{ + Token: testFixtures.token, + Notification: messaging.Notification{ + Title: "My Title", + Body: "This is a Notification", + }, + Apns: messaging.ApnsConfig{ + Headers: map[string]interface{}{ + "private_key": "foo", + "client_email": "bar@test.com", }, }, } - resp, err := client.SendMessage(ctx, msg) + name, err := client.Send(ctx, msg) if err != nil { log.Fatal(err) } - if resp.Name == "" { - t.Errorf("Name : %s; want : projects/%s/messages/#id#", resp.Name, projectID) + if name == "" { + t.Errorf("Name : %s; want : projects/%s/messages/#id#", name, projectID) } } diff --git a/messaging/messaging.go b/messaging/messaging.go index bd658b05..52cf8a5f 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -50,7 +50,7 @@ type Client struct { // RequestMessage is the request body message to send by Firebase Cloud Messaging Service. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send -type RequestMessage struct { +type requestMessage struct { ValidateOnly bool `json:"validate_only,omitempty"` Message Message `json:"message,omitempty"` } @@ -154,15 +154,36 @@ func NewClient(ctx context.Context, c *internal.MessagingConfig) (*Client, error }, nil } -// SendMessage sends a Message to Firebase Cloud Messaging. +// Send sends a Message to Firebase Cloud Messaging. // // Send a message to specified target (a registration token, topic or condition). // https://firebase.google.com/docs/cloud-messaging/send-message -func (c *Client) SendMessage(ctx context.Context, payload *RequestMessage) (msg *ResponseMessage, err error) { - if err := validateTarget(payload); err != nil { - return nil, err +func (c *Client) Send(ctx context.Context, message *Message) (string, error) { + if err := validateMessage(message); err != nil { + return "", err + } + payload := &requestMessage{ + Message: *message, + } + return c.sendRequestMessage(ctx, payload) +} + +// SendDryRun sends a dryRun Message to Firebase Cloud Messaging. +// +// Send a message to specified target (a registration token, topic or condition). +// https://firebase.google.com/docs/cloud-messaging/send-message +func (c *Client) SendDryRun(ctx context.Context, message *Message) (string, error) { + if err := validateMessage(message); err != nil { + return "", err + } + payload := &requestMessage{ + ValidateOnly: true, + Message: *message, } + return c.sendRequestMessage(ctx, payload) +} +func (c *Client) sendRequestMessage(ctx context.Context, payload *requestMessage) (string, error) { versionHeader := internal.WithHeader("X-Client-Version", c.version) request := &internal.Request{ @@ -173,17 +194,17 @@ func (c *Client) SendMessage(ctx context.Context, payload *RequestMessage) (msg } resp, err := c.client.Do(ctx, request) if err != nil { - return nil, err + return "", err } if _, ok := errorCodes[resp.Status]; ok { - return nil, fmt.Errorf("unexpected http status code : %d, reason: %v", resp.Status, string(resp.Body)) + return "", fmt.Errorf("unexpected http status code : %d, reason: %v", resp.Status, string(resp.Body)) } result := &ResponseMessage{} err = resp.Unmarshal(http.StatusOK, result) - return result, err + return result.Name, err } // validators @@ -193,8 +214,13 @@ func (c *Client) SendMessage(ctx context.Context, payload *RequestMessage) (msg // TODO add validator : Conditions for topics support two operators per // expression, and parentheses are supported. -func validateTarget(payload *RequestMessage) error { - if payload.Message.Token == "" && payload.Message.Condition == "" && payload.Message.Topic == "" { +func validateMessage(message *Message) error { + + if message == nil { + return fmt.Errorf("message is empty") + } + + if message.Token == "" && message.Condition == "" && message.Topic == "" { return fmt.Errorf("target is empty you have to fill one of this fields (Token, Condition, Topic)") } return nil diff --git a/messaging/messaging_test.go b/messaging/messaging_test.go index a94762e7..4904ff01 100644 --- a/messaging/messaging_test.go +++ b/messaging/messaging_test.go @@ -33,13 +33,13 @@ func TestEmptyTarget(t *testing.T) { t.Fatal(err) } - _, err = client.SendMessage(ctx, &RequestMessage{}) + _, err = client.Send(ctx, &Message{}) if err == nil { t.Errorf("SendMessage(Message{empty}) = nil; want error") } } -func TestSendMessage(t *testing.T) { +func TestSend(t *testing.T) { var tr *http.Request msgName := "projects/test-project/messages/0:1500415314455276%31bd1c9631bd1c96" ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -55,17 +55,60 @@ func TestSendMessage(t *testing.T) { t.Fatal(err) } client.endpoint = ts.URL - msg, err := client.SendMessage(ctx, &RequestMessage{Message: Message{Topic: "my-topic"}}) + name, err := client.Send(ctx, &Message{Topic: "my-topic"}) if err != nil { t.Errorf("SendMessage() = %v; want nil", err) } - if msg.Name != msgName { - t.Errorf("response Name = %q; want = %q", msg.Name, msgName) + if name != msgName { + t.Errorf("response Name = %q; want = %q", name, msgName) } - if !strings.HasPrefix(msg.Name, "projects/test-project/messages/") { - t.Errorf("response Name = %q; want prefix = %q", msg.Name, "projects/test-project/messages/") + if !strings.HasPrefix(name, "projects/test-project/messages/") { + t.Errorf("response Name = %q; want prefix = %q", name, "projects/test-project/messages/") + } + + if tr.Body == nil { + t.Fatalf("Request = nil; want non-nil") + } + if tr.Method != http.MethodPost { + t.Errorf("Method = %q; want = %q", tr.Method, http.MethodPost) + } + if tr.URL.Path != "/projects/test-project/messages:send" { + t.Errorf("Path = %q; want = %q", tr.URL.Path, "/projects/test-project/messages:send") + } + if h := tr.Header.Get("Authorization"); h != "Bearer test-token" { + t.Errorf("Authorization = %q; want = %q", h, "Bearer test-token") + } +} + +func TestSendDryRun(t *testing.T) { + var tr *http.Request + msgName := "projects/test-project/messages/0:1500415314455276%31bd1c9631bd1c96" + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tr = r + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("{ \"Name\":\"" + msgName + "\" }")) + })) + defer ts.Close() + + ctx := context.Background() + client, err := NewClient(ctx, testMessagingConfig) + if err != nil { + t.Fatal(err) + } + client.endpoint = ts.URL + name, err := client.SendDryRun(ctx, &Message{Topic: "my-topic"}) + if err != nil { + t.Errorf("SendMessage() = %v; want nil", err) + } + + if name != msgName { + t.Errorf("response Name = %q; want = %q", name, msgName) + } + + if !strings.HasPrefix(name, "projects/test-project/messages/") { + t.Errorf("response Name = %q; want prefix = %q", name, "projects/test-project/messages/") } if tr.Body == nil { From 746bfb7c9452741b173eacb410eac25bc7c635c1 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 9 Jan 2018 22:09:06 +0100 Subject: [PATCH 11/17] update fcm url --- messaging/messaging.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messaging/messaging.go b/messaging/messaging.go index 52cf8a5f..acda5f45 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -26,7 +26,7 @@ import ( "google.golang.org/api/transport" ) -const messagingEndpoint = "https://fcm.googleapis.com/v1" +const messagingEndpoint = "https://fcm.googleapis.com/v1/projects/%s/messages:send" var errorCodes = map[int]string{ 400: "malformed argument", @@ -188,7 +188,7 @@ func (c *Client) sendRequestMessage(ctx context.Context, payload *requestMessage request := &internal.Request{ Method: http.MethodPost, - URL: fmt.Sprintf("%s/projects/%s/messages:send", c.endpoint, c.project), + URL: fmt.Sprintf(c.endpoint, c.project), Body: internal.NewJSONEntity(payload), Opts: []internal.HTTPOption{versionHeader}, } From 8a58a72115d1d8f9adeb4275ab976225583c8166 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 9 Jan 2018 22:45:35 +0100 Subject: [PATCH 12/17] rollback url endpoint --- messaging/messaging.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messaging/messaging.go b/messaging/messaging.go index acda5f45..52cf8a5f 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -26,7 +26,7 @@ import ( "google.golang.org/api/transport" ) -const messagingEndpoint = "https://fcm.googleapis.com/v1/projects/%s/messages:send" +const messagingEndpoint = "https://fcm.googleapis.com/v1" var errorCodes = map[int]string{ 400: "malformed argument", @@ -188,7 +188,7 @@ func (c *Client) sendRequestMessage(ctx context.Context, payload *requestMessage request := &internal.Request{ Method: http.MethodPost, - URL: fmt.Sprintf(c.endpoint, c.project), + URL: fmt.Sprintf("%s/projects/%s/messages:send", c.endpoint, c.project), Body: internal.NewJSONEntity(payload), Opts: []internal.HTTPOption{versionHeader}, } From c72d90fa50a5c0987d8e9d7d40f581bf7377110a Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Thu, 18 Jan 2018 13:22:55 +0100 Subject: [PATCH 13/17] fix http constants, change responseMessage visibility, change map[string]interface{} as map[string]string --- messaging/messaging.go | 28 ++++++++++++++-------------- messaging/messaging_test.go | 9 --------- 2 files changed, 14 insertions(+), 23 deletions(-) diff --git a/messaging/messaging.go b/messaging/messaging.go index 52cf8a5f..51ad8fdb 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -55,9 +55,9 @@ type requestMessage struct { Message Message `json:"message,omitempty"` } -// ResponseMessage is the identifier of the message sent. +// responseMessage is the identifier of the message sent. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages -type ResponseMessage struct { +type responseMessage struct { Name string `json:"name"` } @@ -85,12 +85,12 @@ type Notification struct { // AndroidConfig is Android specific options for messages. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#AndroidConfig type AndroidConfig struct { - CollapseKey string `json:"collapse_key,omitempty"` - Priority string `json:"priority,omitempty"` - TTL string `json:"ttl,omitempty"` - RestrictedPackageName string `json:"restricted_package_name,omitempty"` - Data map[string]interface{} `json:"data,omitempty"` - Notification AndroidNotification `json:"notification,omitempty"` + CollapseKey string `json:"collapse_key,omitempty"` + Priority string `json:"priority,omitempty"` + TTL string `json:"ttl,omitempty"` + RestrictedPackageName string `json:"restricted_package_name,omitempty"` + Data map[string]string `json:"data,omitempty"` + Notification AndroidNotification `json:"notification,omitempty"` } // AndroidNotification is notification to send to android devices. @@ -112,9 +112,9 @@ type AndroidNotification struct { // WebpushConfig is Webpush protocol options. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#WebpushConfig type WebpushConfig struct { - Headers map[string]interface{} `json:"headers,omitempty"` - Data map[string]interface{} `json:"data,omitempty"` - Notification WebpushNotification `json:"notification,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Data map[string]string `json:"data,omitempty"` + Notification WebpushNotification `json:"notification,omitempty"` } // WebpushNotification is Web notification to send via webpush protocol. @@ -128,8 +128,8 @@ type WebpushNotification struct { // ApnsConfig is Apple Push Notification Service specific options. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#ApnsConfig type ApnsConfig struct { - Headers map[string]interface{} `json:"headers,omitempty"` - Payload map[string]interface{} `json:"payload,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Payload map[string]string `json:"payload,omitempty"` } // NewClient creates a new instance of the Firebase Cloud Messaging Client. @@ -201,7 +201,7 @@ func (c *Client) sendRequestMessage(ctx context.Context, payload *requestMessage return "", fmt.Errorf("unexpected http status code : %d, reason: %v", resp.Status, string(resp.Body)) } - result := &ResponseMessage{} + result := &responseMessage{} err = resp.Unmarshal(http.StatusOK, result) return result.Name, err diff --git a/messaging/messaging_test.go b/messaging/messaging_test.go index 4904ff01..3ccd9480 100644 --- a/messaging/messaging_test.go +++ b/messaging/messaging_test.go @@ -4,7 +4,6 @@ import ( "context" "net/http" "net/http/httptest" - "strings" "testing" "google.golang.org/api/option" @@ -64,10 +63,6 @@ func TestSend(t *testing.T) { t.Errorf("response Name = %q; want = %q", name, msgName) } - if !strings.HasPrefix(name, "projects/test-project/messages/") { - t.Errorf("response Name = %q; want prefix = %q", name, "projects/test-project/messages/") - } - if tr.Body == nil { t.Fatalf("Request = nil; want non-nil") } @@ -107,10 +102,6 @@ func TestSendDryRun(t *testing.T) { t.Errorf("response Name = %q; want = %q", name, msgName) } - if !strings.HasPrefix(name, "projects/test-project/messages/") { - t.Errorf("response Name = %q; want prefix = %q", name, "projects/test-project/messages/") - } - if tr.Body == nil { t.Fatalf("Request = nil; want non-nil") } From 7091e4b0d41c151967f6772bb49570bdda90ea6e Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Thu, 18 Jan 2018 13:23:32 +0100 Subject: [PATCH 14/17] fix http constants --- messaging/messaging.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/messaging/messaging.go b/messaging/messaging.go index 51ad8fdb..2fc1adbe 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -29,14 +29,14 @@ import ( const messagingEndpoint = "https://fcm.googleapis.com/v1" var errorCodes = map[int]string{ - 400: "malformed argument", - 401: "request not authorized", - 403: "project does not match or the client does not have sufficient privileges", - 404: "failed to find the ...", - 409: "already deleted", - 429: "request throttled out by the backend server", - 500: "internal server error", - 503: "backend servers are over capacity", + http.StatusBadRequest: "malformed argument", + http.StatusUnauthorized: "request not authorized", + http.StatusForbidden: "project does not match or the client does not have sufficient privileges", + http.StatusNotFound: "failed to find the ...", + http.StatusConflict: "already deleted", + http.StatusTooManyRequests: "request throttled out by the backend server", + http.StatusInternalServerError: "internal server error", + http.StatusServiceUnavailable: "backend servers are over capacity", } // Client is the interface for the Firebase Messaging service. From 06195bea0098eb98f1ac779c466355f563772c0a Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Thu, 18 Jan 2018 13:31:35 +0100 Subject: [PATCH 15/17] fix integration tests --- integration/messaging/messaging_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration/messaging/messaging_test.go b/integration/messaging/messaging_test.go index 63f85766..808a2974 100644 --- a/integration/messaging/messaging_test.go +++ b/integration/messaging/messaging_test.go @@ -245,7 +245,7 @@ func TestSendAndroidData(t *testing.T) { CollapseKey: "Collapse", Priority: "HIGH", TTL: "3.5s", - Data: map[string]interface{}{ + Data: map[string]string{ "private_key": "foo", "client_email": "bar@test.com", }, @@ -271,7 +271,7 @@ func TestSendApnsNotification(t *testing.T) { Body: "This is a Notification", }, Apns: messaging.ApnsConfig{ - Payload: map[string]interface{}{ + Payload: map[string]string{ "title": "APNS Title ", "body": "APNS bodym", }, @@ -297,7 +297,7 @@ func TestSendApnsData(t *testing.T) { Body: "This is a Notification", }, Apns: messaging.ApnsConfig{ - Headers: map[string]interface{}{ + Headers: map[string]string{ "private_key": "foo", "client_email": "bar@test.com", }, From 8b2f6bf30982c6330ea857c7eefde7a72af4e58d Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Thu, 25 Jan 2018 23:16:03 +0100 Subject: [PATCH 16/17] fix APNS naming --- integration/messaging/messaging_test.go | 8 ++++---- messaging/messaging.go | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/integration/messaging/messaging_test.go b/integration/messaging/messaging_test.go index 808a2974..99c023ab 100644 --- a/integration/messaging/messaging_test.go +++ b/integration/messaging/messaging_test.go @@ -262,7 +262,7 @@ func TestSendAndroidData(t *testing.T) { } } -func TestSendApnsNotification(t *testing.T) { +func TestSendAPNSNotification(t *testing.T) { ctx := context.Background() msg := &messaging.Message{ Token: testFixtures.token, @@ -270,7 +270,7 @@ func TestSendApnsNotification(t *testing.T) { Title: "My Title", Body: "This is a Notification", }, - Apns: messaging.ApnsConfig{ + APNS: messaging.APNSConfig{ Payload: map[string]string{ "title": "APNS Title ", "body": "APNS bodym", @@ -288,7 +288,7 @@ func TestSendApnsNotification(t *testing.T) { } } -func TestSendApnsData(t *testing.T) { +func TestSendAPNSData(t *testing.T) { ctx := context.Background() msg := &messaging.Message{ Token: testFixtures.token, @@ -296,7 +296,7 @@ func TestSendApnsData(t *testing.T) { Title: "My Title", Body: "This is a Notification", }, - Apns: messaging.ApnsConfig{ + APNS: messaging.APNSConfig{ Headers: map[string]string{ "private_key": "foo", "client_email": "bar@test.com", diff --git a/messaging/messaging.go b/messaging/messaging.go index 2fc1adbe..29d87fdf 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -69,7 +69,7 @@ type Message struct { Notification Notification `json:"notification,omitempty"` Android AndroidConfig `json:"android,omitempty"` Webpush WebpushConfig `json:"webpush,omitempty"` - Apns ApnsConfig `json:"apns,omitempty"` + APNS APNSConfig `json:"apns,omitempty"` Token string `json:"token,omitempty"` Topic string `json:"topic,omitempty"` Condition string `json:"condition,omitempty"` @@ -125,9 +125,9 @@ type WebpushNotification struct { Icon string `json:"icon,omitempty"` } -// ApnsConfig is Apple Push Notification Service specific options. -// See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#ApnsConfig -type ApnsConfig struct { +// APNSConfig is Apple Push Notification Service specific options. +// See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#apnsconfig +type APNSConfig struct { Headers map[string]string `json:"headers,omitempty"` Payload map[string]string `json:"payload,omitempty"` } From 24bdea0d029d99cc33a87133ab05b920c8640e94 Mon Sep 17 00:00:00 2001 From: Cyrille Hemidy Date: Tue, 30 Jan 2018 22:31:18 +0100 Subject: [PATCH 17/17] add validators --- integration/messaging/messaging_test.go | 30 +++--- messaging/messaging.go | 118 ++++++++++++++++++------ 2 files changed, 106 insertions(+), 42 deletions(-) diff --git a/integration/messaging/messaging_test.go b/integration/messaging/messaging_test.go index 99c023ab..e5c3df27 100644 --- a/integration/messaging/messaging_test.go +++ b/integration/messaging/messaging_test.go @@ -72,7 +72,7 @@ func TestSendInvalidToken(t *testing.T) { ctx := context.Background() msg := &messaging.Message{ Token: "INVALID_TOKEN", - Notification: messaging.Notification{ + Notification: &messaging.Notification{ Title: "My Title", Body: "This is a Notification", }, @@ -88,7 +88,7 @@ func TestSendDryRun(t *testing.T) { ctx := context.Background() msg := &messaging.Message{ Token: testFixtures.token, - Notification: messaging.Notification{ + Notification: &messaging.Notification{ Title: "My Title", Body: "This is a Notification", }, @@ -108,7 +108,7 @@ func TestSendToToken(t *testing.T) { ctx := context.Background() msg := &messaging.Message{ Token: testFixtures.token, - Notification: messaging.Notification{ + Notification: &messaging.Notification{ Title: "My Title", Body: "This is a Notification", }, @@ -128,7 +128,7 @@ func TestSendToTopic(t *testing.T) { ctx := context.Background() msg := &messaging.Message{ Topic: testFixtures.topic, - Notification: messaging.Notification{ + Notification: &messaging.Notification{ Title: "My Title", Body: "This is a Notification", }, @@ -148,7 +148,7 @@ func TestSendToCondition(t *testing.T) { ctx := context.Background() msg := &messaging.Message{ Condition: testFixtures.condition, - Notification: messaging.Notification{ + Notification: &messaging.Notification{ Title: "My Title", Body: "This is a Notification", }, @@ -168,7 +168,7 @@ func TestSendNotification(t *testing.T) { ctx := context.Background() msg := &messaging.Message{ Token: testFixtures.token, - Notification: messaging.Notification{ + Notification: &messaging.Notification{ Title: "My Title", Body: "This is a Notification", }, @@ -208,15 +208,15 @@ func TestSendAndroidNotification(t *testing.T) { ctx := context.Background() msg := &messaging.Message{ Token: testFixtures.token, - Notification: messaging.Notification{ + Notification: &messaging.Notification{ Title: "My Title", Body: "This is a Notification", }, - Android: messaging.AndroidConfig{ + Android: &messaging.AndroidConfig{ CollapseKey: "Collapse", Priority: "HIGH", TTL: "3.5s", - Notification: messaging.AndroidNotification{ + Notification: &messaging.AndroidNotification{ Title: "Android Title", Body: "Android body", }, @@ -237,11 +237,11 @@ func TestSendAndroidData(t *testing.T) { ctx := context.Background() msg := &messaging.Message{ Token: testFixtures.token, - Notification: messaging.Notification{ + Notification: &messaging.Notification{ Title: "My Title", Body: "This is a Notification", }, - Android: messaging.AndroidConfig{ + Android: &messaging.AndroidConfig{ CollapseKey: "Collapse", Priority: "HIGH", TTL: "3.5s", @@ -266,11 +266,11 @@ func TestSendAPNSNotification(t *testing.T) { ctx := context.Background() msg := &messaging.Message{ Token: testFixtures.token, - Notification: messaging.Notification{ + Notification: &messaging.Notification{ Title: "My Title", Body: "This is a Notification", }, - APNS: messaging.APNSConfig{ + APNS: &messaging.APNSConfig{ Payload: map[string]string{ "title": "APNS Title ", "body": "APNS bodym", @@ -292,11 +292,11 @@ func TestSendAPNSData(t *testing.T) { ctx := context.Background() msg := &messaging.Message{ Token: testFixtures.token, - Notification: messaging.Notification{ + Notification: &messaging.Notification{ Title: "My Title", Body: "This is a Notification", }, - APNS: messaging.APNSConfig{ + APNS: &messaging.APNSConfig{ Headers: map[string]string{ "private_key": "foo", "client_email": "bar@test.com", diff --git a/messaging/messaging.go b/messaging/messaging.go index 29d87fdf..49752eda 100644 --- a/messaging/messaging.go +++ b/messaging/messaging.go @@ -21,6 +21,9 @@ import ( "errors" "fmt" "net/http" + "regexp" + "strings" + "time" "firebase.google.com/go/internal" "google.golang.org/api/transport" @@ -51,8 +54,8 @@ type Client struct { // RequestMessage is the request body message to send by Firebase Cloud Messaging Service. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages/send type requestMessage struct { - ValidateOnly bool `json:"validate_only,omitempty"` - Message Message `json:"message,omitempty"` + ValidateOnly bool `json:"validate_only,omitempty"` + Message *Message `json:"message,omitempty"` } // responseMessage is the identifier of the message sent. @@ -66,10 +69,10 @@ type responseMessage struct { type Message struct { Name string `json:"name,omitempty"` Data map[string]interface{} `json:"data,omitempty"` - Notification Notification `json:"notification,omitempty"` - Android AndroidConfig `json:"android,omitempty"` - Webpush WebpushConfig `json:"webpush,omitempty"` - APNS APNSConfig `json:"apns,omitempty"` + Notification *Notification `json:"notification,omitempty"` + Android *AndroidConfig `json:"android,omitempty"` + Webpush *WebpushConfig `json:"webpush,omitempty"` + APNS *APNSConfig `json:"apns,omitempty"` Token string `json:"token,omitempty"` Topic string `json:"topic,omitempty"` Condition string `json:"condition,omitempty"` @@ -85,12 +88,12 @@ type Notification struct { // AndroidConfig is Android specific options for messages. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#AndroidConfig type AndroidConfig struct { - CollapseKey string `json:"collapse_key,omitempty"` - Priority string `json:"priority,omitempty"` - TTL string `json:"ttl,omitempty"` - RestrictedPackageName string `json:"restricted_package_name,omitempty"` - Data map[string]string `json:"data,omitempty"` - Notification AndroidNotification `json:"notification,omitempty"` + CollapseKey string `json:"collapse_key,omitempty"` + Priority string `json:"priority,omitempty"` + TTL string `json:"ttl,omitempty"` + RestrictedPackageName string `json:"restricted_package_name,omitempty"` + Data map[string]string `json:"data,omitempty"` + Notification *AndroidNotification `json:"notification,omitempty"` } // AndroidNotification is notification to send to android devices. @@ -112,9 +115,9 @@ type AndroidNotification struct { // WebpushConfig is Webpush protocol options. // See https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages#WebpushConfig type WebpushConfig struct { - Headers map[string]string `json:"headers,omitempty"` - Data map[string]string `json:"data,omitempty"` - Notification WebpushNotification `json:"notification,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Data map[string]string `json:"data,omitempty"` + Notification *WebpushNotification `json:"notification,omitempty"` } // WebpushNotification is Web notification to send via webpush protocol. @@ -163,7 +166,7 @@ func (c *Client) Send(ctx context.Context, message *Message) (string, error) { return "", err } payload := &requestMessage{ - Message: *message, + Message: message, } return c.sendRequestMessage(ctx, payload) } @@ -178,7 +181,7 @@ func (c *Client) SendDryRun(ctx context.Context, message *Message) (string, erro } payload := &requestMessage{ ValidateOnly: true, - Message: *message, + Message: message, } return c.sendRequestMessage(ctx, payload) } @@ -207,21 +210,82 @@ func (c *Client) sendRequestMessage(ctx context.Context, payload *requestMessage return result.Name, err } -// validators - -// TODO add validator : Data messages can have a 4KB maximum payload. -// TODO add validator : topic name reg expression: "[a-zA-Z0-9-_.~%]+". -// TODO add validator : Conditions for topics support two operators per -// expression, and parentheses are supported. - +// validateMessage func validateMessage(message *Message) error { - if message == nil { return fmt.Errorf("message is empty") } - if message.Token == "" && message.Condition == "" && message.Topic == "" { - return fmt.Errorf("target is empty you have to fill one of this fields (Token, Condition, Topic)") + target := bool2int(message.Token != "") + bool2int(message.Condition != "") + bool2int(message.Topic != "") + if target != 1 { + return fmt.Errorf("Exactly one of token, topic or condition must be specified") + } + + // Validate target + if message.Topic != "" { + if strings.HasPrefix(message.Topic, "/topics/") { + return fmt.Errorf("Topic name must not contain the /topics/ prefix") + } + if !regexp.MustCompile("[a-zA-Z0-9-_.~%]+").MatchString(message.Topic) { + return fmt.Errorf("Malformed topic name") + } + } + + // validate AndroidConfig + if message.Android != nil { + if err := validateAndroidConfig(message.Android); err != nil { + return err + } + } + + return nil +} + +func validateAndroidConfig(config *AndroidConfig) error { + if config.TTL != "" && !strings.HasSuffix(config.TTL, "s") { + return fmt.Errorf("ttl must end with 's'") + } + + if _, err := time.ParseDuration(config.TTL); err != nil { + return fmt.Errorf("invalid TTL") + } + + if config.Priority != "" { + if config.Priority != "normal" && config.Priority != "high" { + return fmt.Errorf("priority must be 'normal' or 'high'") + } + } + // validate AndroidNotification + if config.Notification != nil { + if err := validateAndroidNotification(config.Notification); err != nil { + return err + } } return nil } + +func validateAndroidNotification(notification *AndroidNotification) error { + if notification.Color != "" { + if !regexp.MustCompile("^#[0-9a-fA-F]{6}$").MatchString(notification.Color) { + return fmt.Errorf("color must be in the form #RRGGBB") + } + } + if len(notification.TitleLocArgs) > 0 { + if notification.TitleLocKey == "" { + return fmt.Errorf("titleLocKey is required when specifying titleLocArgs") + } + } + if len(notification.BodyLocArgs) > 0 { + if notification.BodyLocKey == "" { + return fmt.Errorf("bodyLocKey is required when specifying bodyLocArgs") + } + } + return nil +} + +func bool2int(b bool) int8 { + if b { + return 1 + } + return 0 +}