Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions lib/force.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ var PasswordExpiredError = errors.New("Password is expired")
var ClassNotFoundError = errors.New("class not found")
var MetricsNotFoundError = errors.New("metrics not found")
var DevHubOrgRequiredError = errors.New("Org must be a Dev Hub")
var ScratchOrgExpiredError = errors.New("Scratch org has expired")

const (
EndpointProduction = iota
Expand Down Expand Up @@ -1553,6 +1554,9 @@ func (f *Force) _makeHttpRequestWithoutRetry(input *httpRequestInput) (*http.Res
// Then if it's a 401/403, treat the session expired (discard the fallback error);
// Finally, return the fallback error.
func (f *Force) _coerceHttpError(res *http.Response, body []byte) error {
if res.StatusCode == 420 {
return ScratchOrgExpiredError
}
var fallbackErr error
sessionExpired := res.StatusCode == 401 || res.StatusCode == 403
if strings.HasPrefix(res.Header.Get("Content-Type"), string(ContentTypeXml)) {
Expand Down Expand Up @@ -1664,6 +1668,9 @@ func (f *Force) httpPostPatch(url string, rbody string, contenttype ContentType,
if res.StatusCode == 401 {
return nil, SessionExpiredError
}
if res.StatusCode == 420 {
return nil, ScratchOrgExpiredError
}

return res, err
}
Expand Down Expand Up @@ -1714,11 +1721,19 @@ func (f *Force) httpPostAttributes(url string, attrs map[string]string) (body []
err = SessionExpiredError
return
}
if res.StatusCode == 420 {
err = ScratchOrgExpiredError
return
}
body, err = ioutil.ReadAll(res.Body)
if res.StatusCode/100 != 2 {
var messages []ForceError
json.Unmarshal(body, &messages)
err = errors.New(messages[0].Message)
if len(messages) > 0 {
err = errors.New(messages[0].Message)
} else {
err = fmt.Errorf("request failed with status %d: %s", res.StatusCode, string(body))
}
emessages = messages
return
}
Expand Down Expand Up @@ -1754,11 +1769,19 @@ func (f *Force) httpPatchAttributes(url string, attrs map[string]string) (body [
err = SessionExpiredError
return
}
if res.StatusCode == 420 {
err = ScratchOrgExpiredError
return
}
body, err = ioutil.ReadAll(res.Body)
if res.StatusCode/100 != 2 {
var messages []ForceError
json.Unmarshal(body, &messages)
err = errors.New(messages[0].Message)
if len(messages) > 0 {
err = errors.New(messages[0].Message)
} else {
err = fmt.Errorf("request failed with status %d: %s", res.StatusCode, string(body))
}
return
}
return
Expand Down Expand Up @@ -1791,11 +1814,19 @@ func (f *Force) httpDeleteUrl(url string) (body []byte, err error) {
err = SessionExpiredError
return
}
if res.StatusCode == 420 {
err = ScratchOrgExpiredError
return
}
body, err = ioutil.ReadAll(res.Body)
if res.StatusCode/100 != 2 {
var messages []ForceError
json.Unmarshal(body, &messages)
err = errors.New(messages[0].Message)
if len(messages) > 0 {
err = errors.New(messages[0].Message)
} else {
err = fmt.Errorf("request failed with status %d: %s", res.StatusCode, string(body))
}
return
}
return
Expand Down
167 changes: 167 additions & 0 deletions lib/force_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,3 +361,170 @@ func TestGetRecord(t *testing.T) {
t.Errorf("Expected Name 'Test Account', got %v", record["Name"])
}
}

func TestCreateRecord_handles_empty_error_response(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal Server Error"))
}))
defer server.Close()

force := &Force{
Credentials: &ForceSession{
InstanceUrl: server.URL,
AccessToken: "test-token",
},
}

_, err, _ := force.CreateRecord("Account", map[string]string{
"Name": "Test Account",
})

if err == nil {
t.Fatal("Expected error for server error response, got nil")
}

if err.Error() != "request failed with status 500: Internal Server Error" {
t.Errorf("Expected error message with status and body, got %q", err.Error())
}
}

func TestUpdateRecord_handles_empty_error_response(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusBadGateway)
w.Write([]byte("Bad Gateway"))
}))
defer server.Close()

force := &Force{
Credentials: &ForceSession{
InstanceUrl: server.URL,
AccessToken: "test-token",
},
}

err := force.UpdateRecord("Account", "001xx000003DGbYAAW", map[string]string{
"Name": "Updated Account",
})

if err == nil {
t.Fatal("Expected error for server error response, got nil")
}

if err.Error() != "request failed with status 502: Bad Gateway" {
t.Errorf("Expected error message with status and body, got %q", err.Error())
}
}

func TestDeleteRecord_handles_empty_error_response(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("Service Unavailable"))
}))
defer server.Close()

force := &Force{
Credentials: &ForceSession{
InstanceUrl: server.URL,
AccessToken: "test-token",
},
}

err := force.DeleteRecord("Account", "001xx000003DGbYAAW")

if err == nil {
t.Fatal("Expected error for server error response, got nil")
}

if err.Error() != "request failed with status 503: Service Unavailable" {
t.Errorf("Expected error message with status and body, got %q", err.Error())
}
}

func TestCreateRecord_returns_ScratchOrgExpiredError_on_420(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(420)
w.Write([]byte("<html>Scratch org expired</html>"))
}))
defer server.Close()

force := &Force{
Credentials: &ForceSession{
InstanceUrl: server.URL,
AccessToken: "test-token",
},
}

_, err, _ := force.CreateRecord("Account", map[string]string{
"Name": "Test Account",
})

if err != ScratchOrgExpiredError {
t.Errorf("Expected ScratchOrgExpiredError, got %v", err)
}
}

func TestUpdateRecord_returns_ScratchOrgExpiredError_on_420(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(420)
w.Write([]byte("<html>Scratch org expired</html>"))
}))
defer server.Close()

force := &Force{
Credentials: &ForceSession{
InstanceUrl: server.URL,
AccessToken: "test-token",
},
}

err := force.UpdateRecord("Account", "001xx000003DGbYAAW", map[string]string{
"Name": "Updated Account",
})

if err != ScratchOrgExpiredError {
t.Errorf("Expected ScratchOrgExpiredError, got %v", err)
}
}

func TestDeleteRecord_returns_ScratchOrgExpiredError_on_420(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(420)
w.Write([]byte("<html>Scratch org expired</html>"))
}))
defer server.Close()

force := &Force{
Credentials: &ForceSession{
InstanceUrl: server.URL,
AccessToken: "test-token",
},
}

err := force.DeleteRecord("Account", "001xx000003DGbYAAW")

if err != ScratchOrgExpiredError {
t.Errorf("Expected ScratchOrgExpiredError, got %v", err)
}
}

func TestGetRecord_returns_ScratchOrgExpiredError_on_420(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(420)
w.Write([]byte("<html>Scratch org expired</html>"))
}))
defer server.Close()

force := &Force{
Credentials: &ForceSession{
InstanceUrl: server.URL,
AccessToken: "test-token",
},
}

_, err := force.GetRecord("Account", "001xx000003DGbYAAW")

if err != ScratchOrgExpiredError {
t.Errorf("Expected ScratchOrgExpiredError, got %v", err)
}
}
67 changes: 62 additions & 5 deletions lib/scratch.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"io/ioutil"
"net/url"
"os"
"time"
)

type ScratchOrg struct {
Expand All @@ -26,19 +27,75 @@ func (s *ScratchOrg) tokenURL() string {
}

func (f *Force) getScratchOrg(scratchOrgId string) (scratchOrg ScratchOrg, err error) {
org, err := f.GetRecord("ScratchOrgInfo", scratchOrgId)
org, err := f.waitForScratchOrgReady(scratchOrgId)
if err != nil {
err = errors.New("Unable to query scratch orgs. You must be logged into a Dev Hub org.")
return
}
username, ok := org["SignupUsername"].(string)
if !ok || username == "" {
err = errors.New("Scratch org is not ready: SignupUsername is not available")
return
}
loginUrl, ok := org["LoginUrl"].(string)
if !ok || loginUrl == "" {
err = errors.New("Scratch org is not ready: LoginUrl is not available")
return
}
authCode, ok := org["AuthCode"].(string)
if !ok || authCode == "" {
err = errors.New("Scratch org is not ready: AuthCode is not available")
return
}
scratchOrg = ScratchOrg{
UserName: org["SignupUsername"].(string),
InstanceUrl: org["LoginUrl"].(string),
AuthCode: org["AuthCode"].(string),
UserName: username,
InstanceUrl: loginUrl,
AuthCode: authCode,
}
return
}

var scratchOrgPollInterval = 5 * time.Second
var scratchOrgMaxWait = 10 * time.Minute

func (f *Force) waitForScratchOrgReady(scratchOrgId string) (org ForceRecord, err error) {
start := time.Now()
for {
org, err = f.GetRecord("ScratchOrgInfo", scratchOrgId)
if err != nil {
err = errors.New("Unable to query scratch orgs. You must be logged into a Dev Hub org.")
return
}

status, _ := org["Status"].(string)
switch status {
case "Active":
return org, nil
case "Error":
errorCode, _ := org["ErrorCode"].(string)
if errorCode != "" {
err = fmt.Errorf("Scratch org creation failed: %s", errorCode)
} else {
err = errors.New("Scratch org creation failed")
}
return
case "New":
if time.Since(start) > scratchOrgMaxWait {
err = errors.New("Timed out waiting for scratch org to become ready")
return
}
fmt.Fprintf(os.Stderr, "Waiting for scratch org to be ready (status: %s)...\n", status)
time.Sleep(scratchOrgPollInterval)
default:
if time.Since(start) > scratchOrgMaxWait {
err = fmt.Errorf("Timed out waiting for scratch org (status: %s)", status)
return
}
fmt.Fprintf(os.Stderr, "Waiting for scratch org to be ready (status: %s)...\n", status)
time.Sleep(scratchOrgPollInterval)
}
}
}

// Log into a Scratch Org
func (f *Force) ForceLoginNewScratch(scratchOrgId string) (session ForceSession, err error) {
scratchOrg, err := f.getScratchOrg(scratchOrgId)
Expand Down
Loading
Loading