Skip to content

Commit

Permalink
Merge pull request #6 from patrickkabwe/fix/client
Browse files Browse the repository at this point in the history
Fix/client
  • Loading branch information
patrickkabwe authored Jul 9, 2024
2 parents 9f7a7f0 + a3878e2 commit c85a302
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 53 deletions.
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,24 @@ client := fcm.NewClient()
Before sending messages, set your service account credentials:

```go
client = client.WithCredentialFile("path/to/serviceAccountKey.json")
client = client.SetCredentialFile("path/to/serviceAccountKey.json")
```
OR

```go
credentials := &fcm.Credentials{
ProjectID: "your-project-id",
PrivateKeyID: "your-private-key-id",
PrivateKey: "yout-private-key",
ClientEmail: "your-client-email",
ClientID: "your-client-id",
AuthURI: "your-auth-uri",
TokenURI: "your-token-uri",
AuthProviderX509CertURL: "your-auth-provider-x509-cert-url",
ClientX509CertURL: "your-client-x509-cert-url",
}

client = client.SetCredential(credentials)
```

### Sending a Message
Expand Down Expand Up @@ -80,7 +97,7 @@ You can customize the HTTP client used for making requests:

```go
customClient := &http.Client{Timeout: time.Second * 10}
client = client.WithHTTPClient(customClient)
client = client.SetHTTPClient(customClient)
```

## Contributing
Expand Down
56 changes: 25 additions & 31 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
Expand All @@ -14,19 +13,6 @@ const (
SCOPES = "https://www.googleapis.com/auth/firebase.messaging"
)

// ServiceAccount represents the credentials for a service account.
type ServiceAccount struct {
Type string `json:"type,omitempty"`
ProjectID string `json:"project_id,omitempty"`
PrivateKeyID string `json:"private_key_id,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
ClientEmail string `json:"client_email,omitempty"`
ClientID string `json:"client_id,omitempty"`
AuthURI string `json:"auth_uri,omitempty"`
TokenURI string `json:"token_uri,omitempty"`
AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url,omitempty"`
ClientX509CertURL string `json:"client_x509_cert_url,omitempty"`
}

// HttpClient is an interface that represents an HTTP client.
type HttpClient interface {
Expand All @@ -35,8 +21,8 @@ type HttpClient interface {

// FCMClient represents a client for interacting with the Firebase Cloud Messaging (FCM) service.
type FCMClient struct {
serviceAccount *ServiceAccount
httpClient HttpClient
credentials *Credentials
httpClient HttpClient
}

// NewClient creates a new FCMClient instance with the default HTTP client.
Expand Down Expand Up @@ -86,35 +72,48 @@ func (f *FCMClient) SendAll(msg *MessagePayload) error {
return f.makeAPICall(msg)
}

// WithCredentialFile sets the service account credentials for the FCM client
// SetCredentialFile sets the service account credentials for the FCM client
// by reading the credentials from the specified file path.
// It returns the modified FCMClient instance.
// If the service account file is not found or there is an error parsing the file,
// it will panic with an appropriate error message.
func (f *FCMClient) WithCredentialFile(serviceAccountFilePath string) *FCMClient {
func (f *FCMClient) SetCredentialFile(serviceAccountFilePath string) *FCMClient {
file, err := os.ReadFile(serviceAccountFilePath)
if os.IsNotExist(err) {
panic("Service account file not found")
}

var serviceAccount ServiceAccount
var serviceAccount Credentials

err = json.Unmarshal(file, &serviceAccount)

if err != nil {
panic("Error parsing service account file")
}

f.serviceAccount = &serviceAccount
f.credentials = &serviceAccount

return f
}

// SetCredentials sets the service account credentials for the FCM client.
func (f *FCMClient) SetCredentials(credentials *Credentials) *FCMClient {
err := credentials.Validate()

if err != nil {
panic(err)
}

f.credentials = credentials

return f
}

// WithHTTPClient sets the HTTP client to be used by the FCM client.
// SetHTTPClient sets the HTTP client to be used by the FCM client.
// It allows you to customize the HTTP client used for making requests to the FCM server.
// The provided httpClient should implement the HttpClient interface.
// Returns the FCM client itself to allow for method chaining.
func (f *FCMClient) WithHTTPClient(httpClient HttpClient) *FCMClient {
func (f *FCMClient) SetHTTPClient(httpClient HttpClient) *FCMClient {
f.httpClient = httpClient
return f
}
Expand All @@ -127,25 +126,22 @@ func (f *FCMClient) makeAPICall(msg *MessagePayload) error {
jsonData, err := json.Marshal(msg)

if err != nil {
log.Println("Error marshalling message payload")
return err
}
req, err := http.NewRequest(
http.MethodPost,
fmt.Sprintf(FCM_V1_URL, f.serviceAccount.ProjectID),
fmt.Sprintf(FCM_V1_URL, f.credentials.ProjectID),
bytes.NewBuffer(jsonData),
)

if err != nil {
log.Println("Error creating API request object", err)
return err
}

req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", f.getAccessToken(f.serviceAccount)))
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", f.getAccessToken(f.credentials)))
res, err := f.httpClient.Do(req)

if err != nil {
log.Println("Error making API request", err)
return err
}

Expand All @@ -157,7 +153,7 @@ func (f *FCMClient) makeAPICall(msg *MessagePayload) error {
// getAccessToken generates and retrieves an access token for the FCM client using the provided service account.
// It first generates a Google JWT using the given service account, then uses the JWT to obtain an access token
// from Google. If any error occurs during the process, an empty string is returned.
func (f *FCMClient) getAccessToken(serviceAccount *ServiceAccount) string {
func (f *FCMClient) getAccessToken(serviceAccount *Credentials) string {
jwt, err := generateGoogleJWT(serviceAccount)

if err != nil {
Expand All @@ -176,7 +172,7 @@ func (f *FCMClient) getAccessToken(serviceAccount *ServiceAccount) string {
func (f *FCMClient) getAccessTokenFromGoogle(jwt string) (string, error) {
req, err := http.NewRequest(
http.MethodPost,
f.serviceAccount.TokenURI,
f.credentials.TokenURI,
bytes.NewBuffer([]byte(fmt.Sprintf("grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=%s", jwt))),
)

Expand Down Expand Up @@ -218,14 +214,12 @@ func (f *FCMClient) handleResponse(res *http.Response) error {
err := json.NewDecoder(res.Body).Decode(&response)

if err != nil {
log.Println("Error decoding response body", err)
return err
}
switch res.StatusCode != http.StatusOK {
case true:
status := response["error"].(map[string]interface{})["status"].(string)
message := response["error"].(map[string]interface{})["message"].(string)
log.Println(status, message)
return fmt.Errorf(`%s: %s`, status, message)
default:
return nil
Expand Down
33 changes: 15 additions & 18 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package fcm

import (
"bytes"
"fmt"
"io"
"net/http"
"testing"
Expand All @@ -27,17 +26,16 @@ func TestNew(t *testing.T) {
if tc.expectedErr {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic", r)
if !tc.expectedErr {
t.Errorf("Expected no panic but got %v", r)
}
}
}()
}
client := NewClient().
WithCredentialFile(tc.serviceFile)
SetCredentialFile(tc.serviceFile)

if !tc.expectedErr && client.serviceAccount == nil {
if !tc.expectedErr && client.credentials == nil {
t.Error("Expected service account to be set")
}
})
Expand Down Expand Up @@ -89,8 +87,8 @@ func TestSend(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := NewClient()
client.WithCredentialFile(testServiceAccountFile).
WithHTTPClient(&testHttpClient{
client.SetCredentialFile(testServiceAccountFile).
SetHTTPClient(&testHttpClient{
DoFunc: tc.doFunc,
})
err := client.Send(tc.payload)
Expand Down Expand Up @@ -149,8 +147,8 @@ func TestSendToTopic(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := NewClient()
client.WithCredentialFile(testServiceAccountFile).
WithHTTPClient(&testHttpClient{
client.SetCredentialFile(testServiceAccountFile).
SetHTTPClient(&testHttpClient{
DoFunc: tc.doFunc,
})
err := client.SendToTopic(tc.payload)
Expand Down Expand Up @@ -209,8 +207,8 @@ func TestSendToCondition(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := NewClient()
client.WithCredentialFile(testServiceAccountFile).
WithHTTPClient(&testHttpClient{
client.SetCredentialFile(testServiceAccountFile).
SetHTTPClient(&testHttpClient{
DoFunc: tc.doFunc,
})
err := client.SendToCondition(tc.payload)
Expand Down Expand Up @@ -269,8 +267,8 @@ func TestSendToMultiple(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := NewClient()
client.WithCredentialFile(testServiceAccountFile).
WithHTTPClient(&testHttpClient{
client.SetCredentialFile(testServiceAccountFile).
SetHTTPClient(&testHttpClient{
DoFunc: tc.doFunc,
})
err := client.SendToMultiple(tc.payload)
Expand All @@ -284,7 +282,6 @@ func TestSendToMultiple(t *testing.T) {
}
}


func TestSendAll(t *testing.T) {
testCases := []struct {
name string
Expand Down Expand Up @@ -352,8 +349,8 @@ func TestSendAll(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := NewClient()
client.WithCredentialFile(testServiceAccountFile).
WithHTTPClient(&testHttpClient{
client.SetCredentialFile(testServiceAccountFile).
SetHTTPClient(&testHttpClient{
DoFunc: tc.doFunc,
})
err := client.SendAll(tc.payload)
Expand All @@ -371,16 +368,16 @@ func TestSendAll(t *testing.T) {
func TestGetAccessToken(t *testing.T) {
resBody := `{"access_token":"test","expires_in":3600}`
client := NewClient().
WithCredentialFile(testServiceAccountFile).
WithHTTPClient(&testHttpClient{
SetCredentialFile(testServiceAccountFile).
SetHTTPClient(&testHttpClient{
DoFunc: func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(bytes.NewReader([]byte(resBody))),
}, nil
},
})
token := client.getAccessToken(client.serviceAccount)
token := client.getAccessToken(client.credentials)

if token == "" {
t.Error("Expected token to be generated")
Expand Down
46 changes: 46 additions & 0 deletions credentials.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package fcm

import "fmt"

// Credentials represents the service account credentials required to authenticate with the FCM server.
type Credentials struct {
Type string `json:"type,omitempty"`
ProjectID string `json:"project_id,omitempty"`
PrivateKeyID string `json:"private_key_id,omitempty"`
PrivateKey string `json:"private_key,omitempty"`
ClientEmail string `json:"client_email,omitempty"`
ClientID string `json:"client_id,omitempty"`
AuthURI string `json:"auth_uri,omitempty"`
TokenURI string `json:"token_uri,omitempty"`
AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url,omitempty"`
ClientX509CertURL string `json:"client_x509_cert_url,omitempty"`
}

// Validate checks if the required fields are set in the credentials.
func (c *Credentials) Validate() error {
if c.ProjectID == "" {
return fmt.Errorf("project_id is required")
}
if c.PrivateKey == "" {
return fmt.Errorf("private_key is required")
}
if c.ClientEmail == "" {
return fmt.Errorf("client_email is required")
}
if c.ClientID == "" {
return fmt.Errorf("client_id is required")
}
if c.AuthURI == "" {
return fmt.Errorf("auth_uri is required")
}
if c.TokenURI == "" {
return fmt.Errorf("token_uri is required")
}
if c.AuthProviderX509CertURL == "" {
return fmt.Errorf("auth_provider_x509_cert_url is required")
}
if c.ClientX509CertURL == "" {
return fmt.Errorf("client_x509_cert_url is required")
}
return nil
}
Loading

0 comments on commit c85a302

Please sign in to comment.