Skip to content

Commit

Permalink
Merge pull request #58 from k-capehart/feature/refresh-sessions
Browse files Browse the repository at this point in the history
Refresh when Invalid Session error occurs
  • Loading branch information
k-capehart authored Aug 5, 2024
2 parents fe7cb16 + 79253d6 commit 5096c4d
Show file tree
Hide file tree
Showing 13 changed files with 619 additions and 272 deletions.
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Returns a new Salesforce instance given a user's credentials.
- `creds`: a struct containing the necessary credentials to authenticate into a Salesforce org
- [Creating a Connected App in Salesforce](https://help.salesforce.com/s/articleView?id=sf.connected_app_create.htm&type=5)
- [Review Salesforce oauth flows](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_flows.htm&type=5)
- If an operation fails with the Error Code `INVALID_SESSION_ID`, go-salesforce will attempt to refresh the session by resubmitting the same credentials used during initialization

[Client Credentials Flow](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_client_credentials_flow.htm&type=5)

Expand Down Expand Up @@ -655,7 +656,7 @@ Create Bulk API Jobs to query, insert, update, upsert, and delete large collecti

- [Review Salesforce REST API resources for Bulk v2](https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/bulk_api_2_0.htm)
- Work with large lists of records by passing either a slice or records or the path to a csv file
- Jobs can run asynchronously and optionally wait for them to finish so errors are available
- Jobs can run asynchronously or synchronously

### QueryBulkExport

Expand Down Expand Up @@ -716,7 +717,7 @@ Inserts a list of salesforce records using Bulk API v2, returning a list of Job
- `sObjectName`: API name of Salesforce object
- `records`: a slice of salesforce records
- `batchSize`: `1 <= batchSize <= 10000`
- `waitForResults`: denotes whether to wait for jobs to finish and return any errors if they are encountered during the operation
- `waitForResults`: denotes whether to wait for jobs to finish

```go
type Contact struct {
Expand Down Expand Up @@ -748,7 +749,7 @@ Inserts a collection of salesforce records from a csv file using Bulk API v2, re
- `sObjectName`: API name of Salesforce object
- `filePath`: path to a csv file containing salesforce data
- `batchSize`: `1 <= batchSize <= 10000`
- `waitForResults`: denotes whether to wait for jobs to finish and return any errors if they are encountered during the operation
- `waitForResults`: denotes whether to wait for jobs to finish

`data/avengers.csv`

Expand Down Expand Up @@ -776,7 +777,7 @@ Updates a list of salesforce records using Bulk API v2, returning a list of Job
- `records`: a slice of salesforce records
- An Id is required
- `batchSize`: `1 <= batchSize <= 10000`
- `waitForResults`: denotes whether to wait for jobs to finish and return any errors if they are encountered during the operation
- `waitForResults`: denotes whether to wait for jobs to finish

```go
type Contact struct {
Expand Down Expand Up @@ -812,7 +813,7 @@ Updates a collection of salesforce records from a csv file using Bulk API v2, re
- `filePath`: path to a csv file containing salesforce data
- An Id is required within csv data
- `batchSize`: `1 <= batchSize <= 10000`
- `waitForResults`: denotes whether to wait for jobs to finish and return any errors if they are encountered during the operation
- `waitForResults`: denotes whether to wait for jobs to finish

`data/update_avengers.csv`

Expand Down Expand Up @@ -844,7 +845,7 @@ Updates (or inserts) a list of salesforce records using Bulk API v2, returning a
- `records`: a slice of salesforce records
- A value for the External Id is required
- `batchSize`: `1 <= batchSize <= 10000`
- `waitForResults`: denotes whether to wait for jobs to finish and return any errors if they are encountered during the operation
- `waitForResults`: denotes whether to wait for jobs to finish

```go
type ContactWithExternalId struct {
Expand Down Expand Up @@ -881,7 +882,7 @@ Updates (or inserts) a collection of salesforce records from a csv file using Bu
- `filePath`: path to a csv file containing salesforce data
- A value for the External Id is required within csv data
- `batchSize`: `1 <= batchSize <= 10000`
- `waitForResults`: denotes whether to wait for jobs to finish and return any errors if they are encountered during the operation
- `waitForResults`: denotes whether to wait for jobs to finish

`data/upsert_avengers.csv`

Expand Down Expand Up @@ -910,7 +911,7 @@ Deletes a list of salesforce records using Bulk API v2, returning a list of Job
- `records`: a slice of salesforce records
- should only contain Ids
- `batchSize`: `1 <= batchSize <= 10000`
- `waitForResults`: denotes whether to wait for jobs to finish and return any errors if they are encountered during the operation
- `waitForResults`: denotes whether to wait for jobs to finish

```go
type Contact struct {
Expand Down Expand Up @@ -943,7 +944,7 @@ Deletes a collection of salesforce records from a csv file using Bulk API v2, re
- `filePath`: path to a csv file containing salesforce data
- should only contain Ids
- `batchSize`: `1 <= batchSize <= 10000`
- `waitForResults`: denotes whether to wait for jobs to finish and return any errors if they are encountered during the operation
- `waitForResults`: denotes whether to wait for jobs to finish

`data/delete_avengers.csv`

Expand Down
56 changes: 53 additions & 3 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ type authentication struct {
Scope string `json:"scope"`
IssuedAt string `json:"issued_at"`
Signature string `json:"signature"`
grantType string
creds Creds
}

type Creds struct {
Expand All @@ -30,8 +32,9 @@ type Creds struct {
}

const (
grantTypePassword = "password"
grantTypeUsernamePassword = "password"
grantTypeClientCredentials = "client_credentials"
grantTypeAccessToken = "access_token"
)

func validateAuth(sf Salesforce) error {
Expand All @@ -45,14 +48,58 @@ func validateSession(auth authentication) error {
if err := validateAuth(Salesforce{auth: &auth}); err != nil {
return err
}
_, err := doRequest(http.MethodGet, "/limits", jsonType, auth, "")
_, err := doRequest(&auth, requestPayload{
method: http.MethodGet,
uri: "/limits",
content: jsonType,
})
if err != nil {
return err
}

return nil
}

func refreshSession(auth *authentication) error {
var refreshedAuth *authentication
var err error

switch grantType := auth.grantType; grantType {
case grantTypeClientCredentials:
refreshedAuth, err = clientCredentialsFlow(
auth.InstanceUrl,
auth.creds.ConsumerKey,
auth.creds.ConsumerSecret,
)
case grantTypeUsernamePassword:
refreshedAuth, err = usernamePasswordFlow(
auth.InstanceUrl,
auth.creds.Username,
auth.creds.Password,
auth.creds.SecurityToken,
auth.creds.ConsumerKey,
auth.creds.ConsumerSecret,
)
default:
return errors.New("invalid session, unable to refresh session")
}

if err != nil {
return err
}

if refreshedAuth == nil {
return errors.New("missing refresh auth")
}

auth.AccessToken = refreshedAuth.AccessToken
auth.IssuedAt = refreshedAuth.IssuedAt
auth.Signature = refreshedAuth.Signature
auth.Id = refreshedAuth.Id

return nil
}

func doAuth(url string, body *strings.Reader) (*authentication, error) {
resp, err := http.Post(url, "application/x-www-form-urlencoded", body)
if err != nil {
Expand All @@ -79,7 +126,7 @@ func doAuth(url string, body *strings.Reader) (*authentication, error) {

func usernamePasswordFlow(domain string, username string, password string, securityToken string, consumerKey string, consumerSecret string) (*authentication, error) {
payload := url.Values{
"grant_type": {grantTypePassword},
"grant_type": {grantTypeUsernamePassword},
"client_id": {consumerKey},
"client_secret": {consumerSecret},
"username": {username},
Expand All @@ -91,6 +138,7 @@ func usernamePasswordFlow(domain string, username string, password string, secur
if err != nil {
return nil, err
}
auth.grantType = grantTypeUsernamePassword
return auth, nil
}

Expand All @@ -106,6 +154,7 @@ func clientCredentialsFlow(domain string, consumerKey string, consumerSecret str
if err != nil {
return nil, err
}
auth.grantType = grantTypeClientCredentials
return auth, nil
}

Expand All @@ -114,5 +163,6 @@ func setAccessToken(domain string, accessToken string) (*authentication, error)
if err := validateSession(*auth); err != nil {
return nil, err
}
auth.grantType = grantTypeAccessToken
return auth, nil
}
72 changes: 72 additions & 0 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func Test_usernamePasswordFlow(t *testing.T) {
Id: "123abc",
IssuedAt: "01/01/1970",
Signature: "signed",
grantType: grantTypeUsernamePassword,
}
server, _ := setupTestServer(auth, http.StatusOK)
defer server.Close()
Expand Down Expand Up @@ -117,6 +118,7 @@ func Test_clientCredentialsFlow(t *testing.T) {
Id: "123abc",
IssuedAt: "01/01/1970",
Signature: "signed",
grantType: grantTypeClientCredentials,
}
server, _ := setupTestServer(auth, http.StatusOK)
defer server.Close()
Expand Down Expand Up @@ -233,3 +235,73 @@ func Test_setAccessToken(t *testing.T) {
})
}
}

func Test_refreshSession(t *testing.T) {
refreshedAuth := authentication{
AccessToken: "1234",
InstanceUrl: "example.com",
Id: "123abc",
IssuedAt: "01/01/1970",
Signature: "signed",
}
serverClientCredentials, sfAuthClientCredentials := setupTestServer(refreshedAuth, http.StatusOK)
defer serverClientCredentials.Close()
sfAuthClientCredentials.grantType = grantTypeClientCredentials

serverUserNamePassword, sfAuthUserNamePassword := setupTestServer(refreshedAuth, http.StatusOK)
defer serverUserNamePassword.Close()
sfAuthUserNamePassword.grantType = grantTypeUsernamePassword

serverNoGrantType, sfAuthNoGrantType := setupTestServer(refreshedAuth, http.StatusOK)
defer serverNoGrantType.Close()

serverBadRequest, sfAuthBadRequest := setupTestServer("", http.StatusBadGateway)
defer serverBadRequest.Close()
sfAuthBadRequest.grantType = grantTypeClientCredentials

serverNoRefresh, sfAuthNoRefresh := setupTestServer("", http.StatusOK)
defer serverNoRefresh.Close()
sfAuthNoRefresh.grantType = grantTypeClientCredentials

type args struct {
auth *authentication
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "refresh_client_credentials",
args: args{auth: &sfAuthClientCredentials},
wantErr: false,
},
{
name: "refresh_username_password",
args: args{auth: &sfAuthUserNamePassword},
wantErr: false,
},
{
name: "error_no_grant_type",
args: args{auth: &sfAuthNoGrantType},
wantErr: true,
},
{
name: "error_bad_request",
args: args{auth: &sfAuthBadRequest},
wantErr: true,
},
{
name: "no_refresh",
args: args{auth: &sfAuthNoRefresh},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := refreshSession(tt.args.auth); (err != nil) != tt.wantErr {
t.Errorf("refreshSession() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Loading

0 comments on commit 5096c4d

Please sign in to comment.